diff --git a/.env.example b/.env.example index cf30f2929f..f982ff7c69 100644 --- a/.env.example +++ b/.env.example @@ -36,12 +36,15 @@ FEATURE_TOGGLE_ENVIRONMENT= # [Optional] Defines the path to external variables file (used on CI) STATIC_CONFIGURATION_FILE_PATH= -# [Optional] disable redux logger during development. Enabled by default, set "1" to disable -DISABLE_DEV_REDUX_LOGGER= +# [Optional] disable redux logger during development. Enabled by default, set "0" to disable +DEV_REDUX_LOGGER_ENABLED= -# [Optional] disable device logger during development. Enabled by default, set "1" to disable -DISABLE_DEV_DEVICE_LOGGER= +# [Optional] disable device logger during development. Enabled by default, set "0" to disable +DEV_DEVICE_LOGGER_ENABLED= # [Optional] set desired latest release. Is based on enum OsEnvironment, so can be set to 'production', 'test-production' or 'daily'. FEATURE_TOGGLE_RELEASE_ENVIRONMENT= + +# [Optional] enable Mudita Center prerelease feature. Disabled by default, set "1" to enable +MUDITA_CENTER_PRERELEASE_ENABLED= diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml index 828c050df8..d6f01fbe3f 100644 --- a/.github/auto_assign.yml +++ b/.github/auto_assign.yml @@ -6,10 +6,10 @@ addAssignees: author # A list of reviewers to be added to pull requests (GitHub user name) reviewers: - - dkarski - lkowalczyk87 - patryk-sierzega - OskarMichalkiewicz + - dkarski # A list of keywords to be skipped the process that add reviewers if pull requests include it skipKeywords: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e7885844cc..c63bac3cbd 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,6 +2,9 @@ Jira: [CP-XXX] **Description** +- [ ] getState function in async thunk actions is correctly typed +- [ ] redux selectors are used in components / prop drilling is reduce +
Screenshots // put images here diff --git a/.gitignore b/.gitignore index b927982d11..f98405031d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,8 @@ packages/app/src/__deprecated__/renderer/fonts/main/**/* # Fetched news packages/app/src/news/default-news.json +# Fetched help items +packages/app/src/help/default-help.json + # Fetched appplication configuration packages/app/src/settings/static/app-configuration.json diff --git a/packages/app/jest.coverage.json b/packages/app/jest.coverage.json index f5f7284316..e944ce0bdb 100644 --- a/packages/app/jest.coverage.json +++ b/packages/app/jest.coverage.json @@ -7,4 +7,4 @@ "branches": 67.71 } } -} +} \ No newline at end of file diff --git a/packages/app/package-lock.json b/packages/app/package-lock.json index d5134867ab..6e34dbf620 100644 --- a/packages/app/package-lock.json +++ b/packages/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "@mudita/mudita-center-app", - "version": "2.0.1", + "version": "2.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -14743,6 +14743,15 @@ "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, + "async-mutex": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.0.tgz", + "integrity": "sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==", + "dev": true, + "requires": { + "tslib": "^2.4.0" + } + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", diff --git a/packages/app/package.json b/packages/app/package.json index 4ff3245e4b..cf8738e35e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@mudita/mudita-center-app", - "version": "2.0.1", + "version": "2.2.0", "description": "Mudita Center", "main": "./dist/main.js", "productName": "Mudita Center", @@ -42,9 +42,10 @@ "translations:sync": "ts-node ./scripts/sync-translations.ts", "fonts:download": "node ./scripts/downloadFonts.js", "news:download": "ts-node ./scripts/downloadNews.ts", + "help:download": "ts-node ./scripts/downloadHelpItems.ts", "app-configuration:download": "ts-node ./scripts/download-configuration.ts", "UTILITY/AUTO COMMANDS": "=================================================", - "postinstall": "npm run fonts:download && npm run news:download && npm run app-configuration:download", + "postinstall": "npm run fonts:download && npm run news:download && npm run app-configuration:download && npm run help:download", "prerelease": "npm run translations:overwrite && npm run build", "prestart": "npm run build", "predist:prod": "npm run translations:sync", @@ -56,7 +57,7 @@ "build": { "productName": "Mudita Center", "appId": "com.mudita.center", - "copyright": "Copyright (c) 2017-2021, Mudita sp. z o.o. All rights reserved.", + "copyright": "Copyright (c) 2017-2023, Mudita sp. z o.o. All rights reserved.", "mac": { "category": "public.app-category.utilities", "icon": "./icons/mac/icon.icns", @@ -190,6 +191,7 @@ "@types/webpack-env": "^1.17.0", "archiver": "^5.3.1", "asar": "^3.2.0", + "async-mutex": "^0.4.0", "awesome-typescript-loader": "^5.2.1", "axios": "^0.27.2", "axios-mock-adapter": "^1.21.2", diff --git a/packages/app/resources/license_en.txt b/packages/app/resources/license_en.txt index 491c7f5e7c..904ddf29a8 100644 --- a/packages/app/resources/license_en.txt +++ b/packages/app/resources/license_en.txt @@ -1,8 +1,112 @@ -Mudita Center Software – Terms of Use +BEFORE INSTALLING MUDITA CENTER, PLEASE READ AND ACCEPT THE REQUIRED DOCUMENTS: PRIVACY POLICY AND TERMS OF USE + +PRIVACY POLICY +WE HEREBY INFORM THAT WE PROCESS YOUR PERSONAL DATA. DETAILS REGARDING THIS CAN BE FOUND BELOW + +Who is the controller of your personal data and who can you contact about it? + +1. The Controller of your personal data is Mudita sp. z o.o. with its registered office in Warsaw at ul. Jana Czeczota 6, 02-607 Warsaw, entered into the Register of Entrepreneurs held by the Regional Court for the Capital City of Warsaw, 12th Commercial Division of the National Court Register, under KRS [National Court Register Number] 0000467620, NIP [Polish Taxpayer Identification Number] 5252558282, REGON [National Business Registration Number] 146767613, share capital:600.000,00 PLN, hereinafter referred to as "Controller or Mudita". + +2. In cases regarding the protection of your personal data and the exercising your rights you can contact us by e-mail: office@mudita.com or in writing to our address indicated in clause 1. +For what purposes and on what grounds do we process your personal data? + +3. Your personal data is processed for the following purposes: +the purpose of the processing & legal basis for the processing +Mudita's software users: + + - sharing and enabling the use of the offered software - indispensability to perform the agreement (Article 6 (1) (b) of the GDPR) + - analyze of data provided - your consent (Article 6 (1) (a) of the GDPR) + +Mudita's website and forum users: + + - necessary cookies - our legitimate interest (Article 6 (1) (f) of the GDPR) + - marketing, functional, analytical cookies or our partners cookies - your consent (Article 6 (1) (a) of the GDPR) + - transferring marketing and promotional information including our partners - our legitimate interest (Article 6 (1) (f) of the GDPR) or your consent (Article 6 (1) (a) of the GDPR) + - performance of the agreement concluded with us (forum users) - indispensability to perform the agreement (Article 6 (1) (b) of the GDPR) + +Sales: + + - leading to the conclusion of the agreement with us - indispensability to perform the agreement (Article 6 (1) (b) of the GDPR) + - performance of the agreement concluded with us and handling complaints concerning the agreements concluded with us - indispensability to perform the agreement (Article 6 (1) (b) of the GDPR) + - performance of additional services - our legitimate interest (Article 6 (1) (f) of the GDPR) + - transferring marketing and promotional information including our partners - our legitimate interest (Article 6 (1) (f) of the GDPR) or your consent (Article 6 (1) (a) of the GDPR) + +questions for us: + + - responding to the questions sent via: contact form, forum, email or phone number - our legitimate interest (Article 6 (1) (f) of the GDPR) + +general: + + - analytical purposes (e.g. selection of the services to the needs of our clients, optimization of our products / services based on your comments on this topic, optimalization of the service processes based on the process of sales service and after-sales service, including complaint, clients' satisfaction survey and determining of the quality of our service) - our legitimate interest (Article 6 (1) (f) of the GDPR) + - possible establishment, investigation or defence against claims (i.e. evidence purposes) - our legitimate interest (Article 6 (1) (f) of the GDPR) + - storage of accounting documents - our obligation to keep accounting documents under tax law + - (Article 6 (1) (c) of the GDPR in conjunction with Article 86 paragraph 1 of the Tax Ordinance Act) + +Who has access to your personal data? + +4. We may share your personal data with the following categories of entities: + + a) employees and associates, + b) related undertakings and cooperating entities, including our partners, + c) entities supporting our activity, including but not limited to legal, accounting, IT, logistics, marketing terms, etc. + How long is your personal data stored? + +5. Your personal data is processed: + + a) in relation to conclusion and performance of the agreement or providing other services (using Mudita's website/forum, software users, products sales, necessary cookies) - for the time necessary to perform the contract; + b) provided under your consent (cookies files or marketing data) - unless you withdraw your consent or further processing will be pointless; + c) related to answering to your inquiries - for the time necessary to perform the obligation and for the time necessary to achieve our goals; + d) for evidence purposes to establish the existence of claims, their pursuit or defence against them - until the end of the limitation period for possible claims in this respect (this period is determined by the provisions of the Polish Civil Code), and in the case of its use in public legal proceedings until the time when after their final termination, extraordinary appeal measures are not be applicable any more (such period shall be determined by the provisions of the Polish Code of Civil Procedure); + e) in connection with the storage of accounting documentation - until the expiry of the limitation period for the tax obligation related to the relevant transaction (this period is determined by the provisions of the Tax Ordinance Act); + What rights do you have in relation to the processing of your personal data? + +6. You shall have the right: + + a) to access to your data and receive a copy of it; + b) to rectify (correct) your data; + c) to delete data: if, in your opinion, there are no grounds for us to process your data, you can request us to delete it; + d) to limit data processing: you can request that we limit the processing of your personal data only to their storage or performance of the activities agreed with you, if in your opinion we have incorrect data about you or we process it unreasonably; or you do not want us to delete it because you need it to establish, pursue or defend claims; or for the duration of your objection to data processing; + e) to object to the processing of data: objection due to special situation - you shall have the right to object to the processing of your data on the basis of a legitimate interest for purposes other than direct marketing, as well as when the processing is necessary for us to fulfil a task carried out in the public interest or the exercising public authority entrusted to us, then you should indicate your special situation, which, in your opinion, justifies the our discontinuation of the processing covered by the objection, we will stop processing your data for such purposes, unless we demonstrate that the grounds for processing your data override your rights or that your data is necessary for us to establish, pursue or defend claims; + f) to transfer data; + + g) to lodge a complaint with the supervisory authority: if you think that we process your data unlawfully, you can lodge a complaint to the supervisory authority responsible for overseeing compliance with the provisions on the protection of personal data (the President of the Office for Personal Data Protection); + + h) to withdraw your consent to the processing of personal data: at any time you shall have the right to withdraw your consent to the processing of your personal data, which we process on the basis of your consent, the withdrawal of consent will not influence the legal compliance of the processing which was performed on the basis of your consent before its withdrawal. + +How to exercise your personal data rights? + +7. In order to exercise your rights, send a request to the contact details indicated in clause 1. Before exercising your rights you shall remember that we will have to make sure it is you, that is, to appropriately identify you. +Is providing personal data mandatory? + +8. Concluding the agreement with us is voluntary. However, providing personal data in connection with the agreement is a condition for its conclusion and then performance - without providing your personal data, it is not possible to conclude the agreement with us. + +Cookies + +9. Cookies are tiny text files that are downloaded to your computer, to improve your experience during using our website. They serve also many functions. They are very important for the proper operation of most websites, including those where we log in to our account. These files identify the computer and the user, they are not malicious programs or associated with any private data. + +We use cookies to improve your experience while you navigate through the our websites accordingly to our cookies policy. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they as essential for the working of basic functionalities of the our websites. + +We also use marketing, functional, analytical or third-party cookies that help us analyze and understand how you use our websites, to store user preferences and provide them with content and advertisements that are relevant to you. These cookies will only be stored on your browser with your consent to do so. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience. + +You can express your consent or objection to the use of cookies after entering our website. Before granting your consent, you can read the full list and details about cookies that we use on our website. + +Details about settings the rules for the use of cookies, including disabling cookies, by the browser are available at the links below: + + - Internet Explorer: https://support.microsoft.com/pl-pl/help/17442/windows-internet-explorer-delete-manage-cookies + - Mozilla Firefox: http://support.mozilla.org/pl/kb/ciasteczka + - Google Chrome: http://support.google.com/chrome/bin/answer.py?hl=pl&answer=95647 + - Opera: http://help.opera.com/Windows/12.10/pl/cookies.html + - Safari: https://support.apple.com/kb/PH5042?locale=en-GB +Additional information + +10. Controller does not share and has no intention to share client's personal data with third country or international organisation. Only except may be the United States (based on standard contractual clauses according to Commission Implementing Decision (EU) 2021/914 of 4 June 2021 on standard contractual clauses for the transfer of personal data to third countries pursuant to Regulation (EU) 2016/679 of the European Parliament and of the Council), which results from the fact, that personal data may be uploaded to the servers of applications, software and IT services providers, located in the United States. + + +MUDITA CENTER SOFTWARE - TERMS OF USE NOTE: BY USING THE MUDITA CENTER SOFTWARE, YOU AGREE TO COMPLY WITH THE TERMS LISTED BELOW. WITHOUT ACCEPTING THEM, YOU WILL NOT ACQUIRE THE RIGHT TO USE THE SOFTWARE. THEREFORE, YOU SHOULD READ THESE TERMS THOROUGHLY BEFORE INSTALLING MUDITA CENTER. IF YOU DO NOT AGREE WITH THESE TERMS, YOU CANNOT OBTAIN AND USE THE SOFTWARE OR DOWNLOAD OR USE ITS UPDATES. - + Definitions: @@ -14,13 +118,13 @@ Definitions: "User" (also referred to as "you") refers to any person who has accepted the Terms of Use for the Mudita Center Software and installed the Software. - + License terms: - Upon your acceptance of these Terms, Mudita will grant you a free license to use the Software. When you download an Update, you will similarly acquire a free license to use the Update. This license grants you a non-exclusive, territorially unlimited right to use the Software and/or Updates without limitation to the number of copies installed on computers, provided that you are entitled to use those computers and install software on them. The license includes: - - storing the Software and/or Updates using the computer’s disk space or on data storage devices, + - storing the Software and/or Updates using the computer's disk space or on data storage devices, - running, displaying, and using the Software and/or Updates on the computer in accordance with their intended purpose. @@ -32,32 +136,32 @@ License terms: - The license shall remain effective until terminated. However, if you violate these Terms in any way, you shall lose the rights to use the Software. Upon losing such rights, you should stop using the Software and destroy any remaining copies. - + Transfer of data: With your permission, Mudita will gain access to information concerning the following errors (general errors, crash dumps, warnings, hard faults; Bluetooth data - state, signal power, controls state; VoLTE - network mode, on/off settings, phone call state; power management - average battery voltage level, minimal and maximal voltage, average current from the battery, state of charge; cellular - SIM slot selected, Mobile Network Code and Mobile Country Code) that may occur while using Mudita Pure and the Software. The aim of accessing such information is to fix errors and further develop the Mudita Pure device and the Software. Information accessed this way will be limited to diagnostic data, including the description of the error, type of operating system, version of the Software, and other technical data, as well as data containing the IP address of the computer that was used to check for Updates for the Software or the Mudita Pure device. No other data will be accessed by Mudita in connection with your use of the Software. - + Limitation of liability: To the maximum extent permitted by the generally applicable laws that apply to the User, Mudita shall not be liable for any losses related to the use of the Software, with the exception of situations resulting from willful misconduct or gross negligence by Mudita. - + Copyright / Third-party services: -Mudita holds all copyrights and licenses for the Software. Certain elements of the Software use or contain software provided by third parties as well as other copyrighted material, which you are entitled to use as part of the Software in accordance with these Terms. The Software may also use certain services provided by third parties. However, before accessing any service, you will be asked to give your permission and accept the terms defined by the service’s provider. +Mudita holds all copyrights and licenses for the Software. Certain elements of the Software use or contain software provided by third parties as well as other copyrighted material, which you are entitled to use as part of the Software in accordance with these Terms. The Software may also use certain services provided by third parties. However, before accessing any service, you will be asked to give your permission and accept the terms defined by the service's provider. + - Amendments to the Terms: -Mudita reserves the right to amend these Terms under justified circumstances (such as changes to the applicable law, the Software’s functionality, or the nature of Mudita’s business). The updated Terms will always be available in the corresponding tab of the Software as well as on the Mudita website. Choosing not to uninstall the Software upon receiving information about amendments to the Terms will be deemed to constitute acceptance of the changes. +Mudita reserves the right to amend these Terms under justified circumstances (such as changes to the applicable law, the Software's functionality, or the nature of Mudita's business). The updated Terms will always be available in the corresponding tab of the Software as well as on the Mudita website. Choosing not to uninstall the Software upon receiving information about amendments to the Terms will be deemed to constitute acceptance of the changes. + - Applicable law: -The agreement entered into by accepting these Terms is governed by the laws of the Republic of Poland. However, it does not deprive the consumer of protection granted by the legal regulations of their country of habitual residence that cannot be derogated from by agreement if the regulations applicable in that country are more beneficial to the consumer than the regulations of the Republic of Poland. \ No newline at end of file +The agreement entered into by accepting these Terms is governed by the laws of the Republic of Poland. However, it does not deprive the consumer of protection granted by the legal regulations of their country of habitual residence that cannot be derogated from by agreement if the regulations applicable in that country are more beneficial to the consumer than the regulations of the Republic of Poland. diff --git a/packages/app/scripts/downloadHelpItems.ts b/packages/app/scripts/downloadHelpItems.ts new file mode 100644 index 0000000000..72d6d26b86 --- /dev/null +++ b/packages/app/scripts/downloadHelpItems.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +const axios = require("axios") +const path = require("path") +const fs = require("fs-extra") +require("dotenv").config({ + path: path.join(__dirname, "../../../.env"), +}) +import { normalizeHelpData } from "../../app/src/__deprecated__/renderer/utils/contentful/normalize-help-data" +;(async () => { + try { + await fs.ensureDir(path.resolve(path.join("src", "main"))) + const jsonPath = path.join("src", "help", "default-help.json") + + const url = `${process.env.MUDITA_CENTER_SERVER_URL}/help` + const { data } = await axios.get(url) + const helpData = normalizeHelpData(data, "en-US") + + await fs.writeJson(path.resolve(jsonPath), helpData) + console.log("Help downloading finished.") + } catch (error) { + console.log("Error while downloading Help.", error) + } +})() diff --git a/packages/app/src/__deprecated__/api/mudita-center-server/client.ts b/packages/app/src/__deprecated__/api/mudita-center-server/client.ts index 422c6b7a28..1320d8f3c1 100644 --- a/packages/app/src/__deprecated__/api/mudita-center-server/client.ts +++ b/packages/app/src/__deprecated__/api/mudita-center-server/client.ts @@ -20,6 +20,15 @@ export interface getLatestProductionReleaseParams { product: Product version: "latest" | string environment: OsEnvironment + deviceSerialNumber?: string +} + +interface ExternalUsageDeviceQueryParams { + "serial-number": string +} + +interface ExternalUsageDeviceResponse { + externalUsage: boolean } export class Client implements ClientInterface { @@ -83,6 +92,23 @@ export class Client implements ClientInterface { } } + async getExternalUsageDevice(serialNumber: string): Promise { + try { + const params: ExternalUsageDeviceQueryParams = { + "serial-number": serialNumber, + } + + const response = await this.httpClient.get< + ExternalUsageDeviceQueryParams, + AxiosResponse + >(MuditaCenterServerRoutes.ExternalUsageDevice, { params }) + + return response.data.externalUsage + } catch (_) { + return false + } + } + async getLatestRelease( params: getLatestProductionReleaseParams ): Promise> { diff --git a/packages/app/src/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts b/packages/app/src/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts index 46018d4cc5..d12707a390 100644 --- a/packages/app/src/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts +++ b/packages/app/src/__deprecated__/api/mudita-center-server/mudita-center-server-routes.ts @@ -8,4 +8,5 @@ export enum MuditaCenterServerRoutes { Help = "help", GetReleaseV2 = "v2-get-release", AppConfigurationV2 = "v2-app-configuration", + ExternalUsageDevice = "external-usage-device", } diff --git a/packages/app/src/__deprecated__/common/enums/about-actions.enum.ts b/packages/app/src/__deprecated__/common/enums/about-actions.enum.ts index edcfc48558..cb0c782b10 100644 --- a/packages/app/src/__deprecated__/common/enums/about-actions.enum.ts +++ b/packages/app/src/__deprecated__/common/enums/about-actions.enum.ts @@ -7,4 +7,5 @@ export enum AboutActions { LicenseOpenWindow = "license-open-window", TermsOpenWindow = "terms-open-window", PolicyOpenWindow = "policy-open-window", + PolicyOpenBrowser = "policy-open-browser", } diff --git a/packages/app/src/__deprecated__/common/enums/browser-actions.enum.ts b/packages/app/src/__deprecated__/common/enums/browser-actions.enum.ts new file mode 100644 index 0000000000..daa14d3eed --- /dev/null +++ b/packages/app/src/__deprecated__/common/enums/browser-actions.enum.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum BrowserActions { + PolicyOpenBrowser = "policy-open-browser", + UpdateOpenBrowser = "update-open-browser", +} diff --git a/packages/app/src/__deprecated__/main/main.ts b/packages/app/src/__deprecated__/main/main.ts index 3a70f9a2d4..72c6446cc3 100644 --- a/packages/app/src/__deprecated__/main/main.ts +++ b/packages/app/src/__deprecated__/main/main.ts @@ -69,6 +69,7 @@ import { Mode } from "App/__deprecated__/common/enums/mode.enum" import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" import { AboutActions } from "App/__deprecated__/common/enums/about-actions.enum" import { PureSystemActions } from "App/__deprecated__/common/enums/pure-system-actions.enum" +import { BrowserActions } from "App/__deprecated__/common/enums/browser-actions.enum" import { createMetadataStore, MetadataStore, @@ -80,6 +81,7 @@ import { import { registerOsUpdateAlreadyDownloadedCheck } from "App/update/requests" import { createSettingsService } from "App/settings/containers/settings.container" import { ApplicationModule } from "App/core/application.module" +import registerExternalUsageDevice from "App/device/listeners/register-external-usage-device.listner" // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call @@ -131,7 +133,6 @@ const commonWindowOptions: BrowserWindowConstructorOptions = { webSecurity: false, devTools: !productionEnvironment, }, - autoHideMenuBar: true, } const getWindowOptions = ( extendedWindowOptions?: BrowserWindowConstructorOptions @@ -162,6 +163,13 @@ const createWindow = async () => { title, }) ) + win.removeMenu() + + win.webContents.on("before-input-event", (event, input) => { + if ((input.control || input.meta) && input.key.toLowerCase() === "r") { + event.preventDefault() + } + }) win.on("closed", () => { win = null @@ -195,6 +203,7 @@ const createWindow = async () => { registerMetadataAllGetValueListener() registerMetadataGetValueListener() registerMetadataSetValueListener() + registerExternalUsageDevice() if (productionEnvironment) { win.setMenuBarVisibility(false) @@ -268,6 +277,7 @@ ipcMain.answerRenderer(HelpActions.OpenWindow, () => { title, }) ) + helpWindow.removeMenu() helpWindow.on("closed", () => { removeDownloadHelpHandler() @@ -308,13 +318,14 @@ const createOpenWindowListener = ( if (newWindow === null) { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/await-thenable - newWindow = await new BrowserWindow( + newWindow = new BrowserWindow( getWindowOptions({ width: DEFAULT_WINDOWS_SIZE.width, height: DEFAULT_WINDOWS_SIZE.height, title, }) ) + newWindow.removeMenu() newWindow.on("closed", () => { newWindow = null @@ -343,6 +354,15 @@ const createOpenWindowListener = ( }) } +ipcMain.answerRenderer(BrowserActions.PolicyOpenBrowser, () => + shell.openExternal( + `${process.env.MUDITA_CENTER_SERVER_URL ?? ""}/privacy-policy-url` + ) +) +ipcMain.answerRenderer(BrowserActions.UpdateOpenBrowser, () => + shell.openExternal("https://mudita.com") +) + createOpenWindowListener( AboutActions.LicenseOpenWindow, Mode.License, @@ -412,6 +432,7 @@ ipcMain.answerRenderer(GoogleAuthActions.OpenWindow, async (scope: Scope) => { title, }) ) + googleAuthWindow.removeMenu() googleAuthWindow.on("close", () => { void ipcMain.callRenderer( @@ -474,6 +495,7 @@ ipcMain.answerRenderer( title, }) ) + outlookAuthWindow.removeMenu() // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/app/src/__deprecated__/main/store/default-help-items.ts b/packages/app/src/__deprecated__/main/store/default-help-items.ts deleted file mode 100644 index b4b08389de..0000000000 --- a/packages/app/src/__deprecated__/main/store/default-help-items.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { QuestionAndAnswer } from "App/help/components/help.component" -import { Document } from "@contentful/rich-text-types" -import { BLOCKS } from "@contentful/rich-text-types" - -const answer: Document = { - nodeType: BLOCKS.DOCUMENT, - data: {}, - content: [ - { - nodeType: BLOCKS.HEADING_2, - content: [ - { - nodeType: "text", - value: - "Consectetur adipiscing elit. Fusce imperdiet nisi odio, et iaculis justo sagittis non.", - marks: [], - data: {}, - }, - ], - data: {}, - }, - { - nodeType: BLOCKS.PARAGRAPH, - content: [ - { - nodeType: "text", - value: - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus diam neque, varius ac fermentum sit amet, interdum in metus. Vivamus eleifend turpis nec accumsan mollis.", - marks: [], - data: {}, - }, - ], - data: {}, - }, - { - nodeType: BLOCKS.PARAGRAPH, - content: [ - { - nodeType: "text", - value: - "Sed nunc erat, tempor vel risus nec, consectetur lobortis lectus. Maecenas ultricies ex mi, quis consequat est cursus ut. Phasellus ut ante quis metus lacinia lacinia a non ante. Etiam ut libero sit amet sem rutrum mollis quis sed sapien. Donec vitae lacus vitae odio auctor rhoncus et sed ipsum. Pellentesque ac viverra turpis. Aliquam posuere lorem non orci placerat venenatis. Mauris posuere consectetur orci sed sodales.", - marks: [], - data: {}, - }, - ], - data: {}, - }, - { - nodeType: BLOCKS.HEADING_3, - content: [ - { - nodeType: "text", - value: "Maecenas ultricies ex mi, quis consequat est cursus ut.", - marks: [], - data: {}, - }, - ], - data: {}, - }, - { - nodeType: BLOCKS.PARAGRAPH, - content: [ - { - nodeType: "text", - value: - "Sed nunc erat, tempor vel risus nec, consectetur lobortis lectus. Maecenas ultricies ex mi, quis consequat est cursus ut. Phasellus ut ante quis metus lacinia lacinia a non ante. Etiam ut libero sit amet sem rutrum mollis quis sed sapien. Donec vitae lacus vitae odio auctor rhoncus et sed ipsum.", - marks: [], - data: {}, - }, - ], - data: {}, - }, - { - nodeType: BLOCKS.PARAGRAPH, - content: [ - { - nodeType: "text", - value: "", - marks: [], - data: {}, - }, - ], - data: {}, - }, - ], -} - -export const defaultHelpItems = { - collection: ["24YEjwJx8jAuedvWDz8rvU", "1NESOKKWZCTjV8rlSE4JbH"], - items: { - "24YEjwJx8jAuedvWDz8rvU": { - id: "24YEjwJx8jAuedvWDz8rvU", - question: "Saepe non quasi at ipsa autem molestias et consequuntur.", - answer, - }, - "1NESOKKWZCTjV8rlSE4JbH": { - id: "1NESOKKWZCTjV8rlSE4JbH", - question: "Saepe non quasi at ipsa autem molestias et consequuntur.", - answer, - }, - }, -} - -export const getDefaultHelpItems = (): QuestionAndAnswer => defaultHelpItems diff --git a/packages/app/src/__deprecated__/main/utils/logger.ts b/packages/app/src/__deprecated__/main/utils/logger.ts index 04f0f5175b..e4bc0c7f1c 100644 --- a/packages/app/src/__deprecated__/main/utils/logger.ts +++ b/packages/app/src/__deprecated__/main/utils/logger.ts @@ -51,9 +51,13 @@ const createDailyRotateFileTransport = ( .map(([key, value]) => (value ? `${key}: ${value}` : "")) .filter((item) => item) .join("\n")}` + + // eslint-disable-next-line + const shortMessage = message.toString().slice(0, 50000) + // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - return `${paddedProcess} [${timestamp}] ${paddedLevel} ${message} ${metadata}` + return `${paddedProcess} [${timestamp}] ${paddedLevel} ${shortMessage} ${metadata}` }) ) // AUTO DISABLED - fix me if you like :) diff --git a/packages/app/src/__deprecated__/passcode-modal/passcode-modal-ui.component.tsx b/packages/app/src/__deprecated__/passcode-modal/passcode-modal-ui.component.tsx index 6f0d9cff02..5f9d079672 100644 --- a/packages/app/src/__deprecated__/passcode-modal/passcode-modal-ui.component.tsx +++ b/packages/app/src/__deprecated__/passcode-modal/passcode-modal-ui.component.tsx @@ -5,11 +5,8 @@ import React from "react" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { ModalContent } from "App/settings/components/collecting-data-modal/collecting-data-modal.styled" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalDialog, ModalDialogProps } from "App/ui/components/modal-dialog" import styled from "styled-components" -import theme from "App/__deprecated__/renderer/styles/theming/theme" -import { zIndex } from "App/__deprecated__/renderer/styles/theming/theme-getters" import Icon, { IconSize, } from "App/__deprecated__/renderer/components/core/icon/icon.component" @@ -20,6 +17,11 @@ import PasscodeLocked from "App/__deprecated__/passcode-modal/components/Passcod import { PasscodeModalTestIds } from "App/__deprecated__/passcode-modal/passcode-modal-test-ids.enum" import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +const ModalContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; +` const LogoWrapper = styled.div` display: flex; justify-content: center; @@ -48,7 +50,8 @@ export const PasscodeModalContent = styled(ModalContent)` height: clamp(28rem, 60vh, 46.4rem); ` -export interface PasscodeModalProps { +export interface PasscodeModalProps + extends Omit { openModal: boolean close: () => void values: string[] @@ -91,7 +94,6 @@ const PasscodeModalUI: FunctionComponent = ({ closeButton={false} closeModal={canBeClosed ? close : undefined} title={muditaLogo} - zIndex={zIndex("passCodeModal")({ theme })} > diff --git a/packages/app/src/__deprecated__/passcode-modal/passcode-modal.component.tsx b/packages/app/src/__deprecated__/passcode-modal/passcode-modal.component.tsx index a8b9746834..24aec79cfe 100644 --- a/packages/app/src/__deprecated__/passcode-modal/passcode-modal.component.tsx +++ b/packages/app/src/__deprecated__/passcode-modal/passcode-modal.component.tsx @@ -10,8 +10,10 @@ import PasscodeModalUI from "./passcode-modal-ui.component" import { ipcRenderer } from "electron-better-ipc" import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" import { AppError } from "App/core/errors" +import { ModalDialogProps } from "App/ui" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" -interface Props { +interface Props extends Omit { openModal: boolean close: () => void leftTime?: number @@ -43,6 +45,7 @@ const PasscodeModal: FunctionComponent = ({ leftTime, unlockDevice, getUnlockStatus, + ...rest }) => { const initValue = ["", "", "", ""] const [errorState, setErrorState] = useState(ErrorState.NoError) @@ -133,6 +136,8 @@ const PasscodeModal: FunctionComponent = ({ onNotAllowedKeyDown={onNotAllowedKeyDown} leftTime={leftTime} canBeClosed={canBeClosed} + layer={ModalLayers.Passcode} + {...rest} /> ) } diff --git a/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.component.tsx b/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.component.tsx index 789a74e694..f9dc545031 100644 --- a/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.component.tsx +++ b/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.component.tsx @@ -29,15 +29,15 @@ const getInteractiveBatteryIcon = ( return case batteryLevel === 1 && deviceType === DeviceType.MuditaPure: return - case batteryLevel > 0.8: + case batteryLevel > 0.95: return - case batteryLevel > 0.6: + case batteryLevel >= 0.7: return - case batteryLevel > 0.4: + case batteryLevel >= 0.4: return - case batteryLevel > 0.2: + case batteryLevel >= 0.2: return - case batteryLevel > 0: + case batteryLevel >= 0.1: return default: return diff --git a/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.test.tsx b/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.test.tsx index 24a2ce42ae..9ce73705df 100644 --- a/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.test.tsx +++ b/packages/app/src/__deprecated__/renderer/components/core/icon/battery-icon.test.tsx @@ -25,19 +25,19 @@ describe("battery icon returns correct component", () => { dataTestId: "icon-VeryLowBattery", }, { - batteryLevel: 0.21, + batteryLevel: 0.31, dataTestId: "icon-LowBattery", }, { - batteryLevel: 0.41, + batteryLevel: 0.51, dataTestId: "icon-MediumBattery", }, { - batteryLevel: 0.61, + batteryLevel: 0.71, dataTestId: "icon-HighBattery", }, { - batteryLevel: 0.9, + batteryLevel: 0.96, dataTestId: "icon-VeryHighBattery", }, ] diff --git a/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.elements.tsx b/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.elements.tsx index 8dbd3917e2..b36a416482 100644 --- a/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.elements.tsx +++ b/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.elements.tsx @@ -63,10 +63,17 @@ const errorStyles = css` border-color: ${borderColor("error")}; ` -export const InputError = styled(Text)<{ visible: boolean }>` - position: absolute; - left: 0; - top: 100%; +export const InputError = styled(Text)<{ + visible: boolean + relative?: boolean +}>` + ${({ relative }) => + !relative && + css` + position: absolute; + left: 0; + top: 100%; + `} width: 100%; margin-top: 0.4rem; color: ${textColor("error")}; @@ -378,6 +385,7 @@ export const InputText: FunctionComponent = ({ errorMessage, focusable, initialTransparentBorder = false, + relativeError, ...rest }) => { const standardInput = ( @@ -405,21 +413,32 @@ export const InputText: FunctionComponent = ({ ) return ( - - {outlined ? outlinedInput : standardInput} - - {errorMessage} - + <> + + {outlined ? outlinedInput : standardInput} + + {!relativeError && ( + + {errorMessage} + + )} + + {relativeError && ( + + {errorMessage} + + )} + ) } @@ -439,6 +458,7 @@ export const TextArea: FunctionComponent = ({ errorMessage, focusable, defaultHeight, + relativeError, ...rest }) => { const textareaRef = useRef(null) @@ -511,18 +531,29 @@ export const TextArea: FunctionComponent = ({ ) return ( - - {outlined ? standardTextarea : inputLikeTextarea} - - {errorMessage} - + <> + + {outlined ? standardTextarea : inputLikeTextarea} + + {!relativeError && ( + + {errorMessage} + + )} + + {relativeError && ( + + {errorMessage} + + )} + ) } diff --git a/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.interface.ts b/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.interface.ts index e164750a1a..aa678b00ef 100644 --- a/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.interface.ts +++ b/packages/app/src/__deprecated__/renderer/components/core/input-text/input-text.interface.ts @@ -34,6 +34,7 @@ export interface InputProps inputRef?: Ref type: "text" | "email" | "password" | "search" | "tel" | "url" initialTransparentBorder?: boolean + relativeError?: boolean } export interface TextareaProps @@ -42,6 +43,7 @@ export interface TextareaProps maxRows?: number inputRef?: Ref type: "textarea" + relativeError?: boolean } export interface InputPasscodeProps extends TextareaHTMLAttributes, diff --git a/packages/app/src/__deprecated__/renderer/components/rest/menu/menu.component.tsx b/packages/app/src/__deprecated__/renderer/components/rest/menu/menu.component.tsx index b7d95d0f86..d7ebae0aab 100644 --- a/packages/app/src/__deprecated__/renderer/components/rest/menu/menu.component.tsx +++ b/packages/app/src/__deprecated__/renderer/components/rest/menu/menu.component.tsx @@ -131,8 +131,9 @@ const Menu: FunctionComponent = ({ {syncState !== undefined && (syncState === SynchronizationState.Loading || syncState === SynchronizationState.Cache || - synchronizationProcess === - SynchronizationProcessState.InProgress) && ( + synchronizationProcess === SynchronizationProcessState.InProgress) && + //CP-1668 - this condition until Kompakt has limited endpoint support, currently only device info endpoint (10.08.2023) + deviceType !== DeviceType.MuditaKompakt && ( diff --git a/packages/app/src/__deprecated__/renderer/constants/tab-elements.ts b/packages/app/src/__deprecated__/renderer/constants/tab-elements.ts index c66f85c173..3da4751968 100644 --- a/packages/app/src/__deprecated__/renderer/constants/tab-elements.ts +++ b/packages/app/src/__deprecated__/renderer/constants/tab-elements.ts @@ -105,11 +105,6 @@ export const tabElements: TabElement[] = [ { parentUrl: URL_MAIN.settings, tabs: [ - { - label: messages.connection, - url: URL_MAIN.settings, - icon: IconType.Connection, - }, { label: messages.notifications, url: `${URL_MAIN.settings}${URL_TABS.notifications}`, @@ -124,7 +119,7 @@ export const tabElements: TabElement[] = [ }, { label: messages.backup, - url: `${URL_MAIN.settings}${URL_TABS.backup}`, + url: URL_MAIN.settings, icon: IconType.BackupFolder, }, { diff --git a/packages/app/src/__deprecated__/renderer/constants/urls.ts b/packages/app/src/__deprecated__/renderer/constants/urls.ts index fe33086a38..9539d03b1a 100644 --- a/packages/app/src/__deprecated__/renderer/constants/urls.ts +++ b/packages/app/src/__deprecated__/renderer/constants/urls.ts @@ -33,7 +33,6 @@ export const URL_TABS = { connection: "/connection", notifications: "/notifications", audioConversion: "/audio-conversion", - backup: "/backup", about: "/about", } as const diff --git a/packages/app/src/__deprecated__/renderer/interfaces/message.interface.ts b/packages/app/src/__deprecated__/renderer/interfaces/message.interface.ts index 7fb37f45d6..35948babf7 100755 --- a/packages/app/src/__deprecated__/renderer/interfaces/message.interface.ts +++ b/packages/app/src/__deprecated__/renderer/interfaces/message.interface.ts @@ -15,5 +15,5 @@ export interface Message { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/ban-types readonly description?: object | string - readonly values?: Record + readonly values?: Record } diff --git a/packages/app/src/__deprecated__/renderer/locales/default/en-US.json b/packages/app/src/__deprecated__/renderer/locales/default/en-US.json index cb1146b6b9..30285250ab 100644 --- a/packages/app/src/__deprecated__/renderer/locales/default/en-US.json +++ b/packages/app/src/__deprecated__/renderer/locales/default/en-US.json @@ -102,8 +102,8 @@ "component.formErrorNumbersOnly": "May contain only numbers and + sign", "component.formErrorNumberUnique": "The phone number must be unique", "component.formErrorRequiredEmail": "Email is required", - "component.formErrorTooLong": "Must be shorter than {maxLength} characters", "component.formErrorRequiredPrimaryPhone": "Phone number is required", + "component.formErrorTooLong": "Must be shorter than {maxLength} characters", "component.formErrorTooManyFiles": "You tried to attach more than {limit} files.", "component.formErrorTooShort": "Must be longer than {minLength} characters", "component.formErrorTypeNotAllowed": "File {name} has wrong type. File types allowed are: {extensions}.", @@ -129,7 +129,7 @@ "component.passcodeModalTryAgain": "Please try again", "component.rowCheckTooltipDescription": "Select the row", "component.rowUncheckTooltipDescription": "Unselect the row", - "component.selectionSelectAllTooltipDescription": "Select all rows", + "component.selectionSelectAllTooltipDescription": "Select all row", "component.selectionUnselectAllTooltipDescription": "Unselect all rows", "component.supportButton": "Contact support", "component.supportModalActionButton": "Send", @@ -168,10 +168,10 @@ "component.textToday": "Today", "component.textWeekly": "Weekly", "component.textYearly": "Yearly", - "component.updateAvailableModalButton": "Download", + "component.updateAvailableModalButton": "Update", "component.updateAvailableModalCloseButton": "Update Later", - "component.updateAvailableModalDescription": "Download the latest version of Mudita Center to make sure that your software is up to date.", - "component.updateAvailableModalMessage": "New Update is available!", + "component.updateAvailableModalDescription": "To be able to fully use the application, please agree to the Privacy Policy and update Mudita Center.", + "component.updateAvailableModalMessage": "Update Mudita Center to {version}", "component.updateAvailableModalNoVersionMessage": "Update Mudita Center", "component.updateAvailableModalVersion": "Mudita Center {version}", "component.updateDownloadCloseButton": "Cancel", @@ -185,9 +185,9 @@ "component.updateForcedModalButton": "Download", "component.updateForcedModalCurrentVersion": "Your current version: v.{version}", "component.updateForcedModalDescription": "to use the full version of the Mudita Center.", - "component.updateForcedModalMessage": "Update your Mudita Center", + "component.updateForcedModalMessage": "Update Mudita Center to {version}", "component.updateForcedModalNoVersionMessage": "Update Mudita Center", - "component.updateForcedModalVersion": "Mudita Center {version} version is available. Please update it", + "component.updateForcedModalVersion": "Update it to use the full version of the Mudita Center. Your current version: v.{version}", "component.updateForcedNewVersionDescription": "Mudita Center {version} version is available. Please update it", "component.updateForcedPrivacyPolicy": "Please accept the Privacy Policy to start updating Mudita Center.", "component.updateModalTitle": "Mudita Center", @@ -261,10 +261,10 @@ "module.connecting.criticalBatteryLevelModalDescription": "Charge your Pure, then disconnect and reconnect your phone to use Mudita Center.", "module.connecting.criticalBatteryLevelModalHeaderTitle": "MuditaOS", "module.connecting.criticalBatteryLevelModalTitle": "The battery in your Pure is flat.", - "module.connecting.errorConnectingDescription": "Please restart your device and try again", - "module.connecting.errorConnectingModalHeaderTitle": "Connecting Error", + "module.connecting.errorConnectingDescription": "If you still have problems connecting to your device, have a look at our {link}", + "module.connecting.errorConnectingModalHeaderTitle": "We couldn't connect to your device...", "module.connecting.errorConnectingModalSecondaryButton": "Cancel", - "module.connecting.errorConnectingModalTitle": "Your device is not responding", + "module.connecting.errorConnectingModalTitle": "Make sure your device is plugged in using a USB C cable and restart Mudita Center or your computer to try again", "module.connecting.errorSyncModalButton": "Try again", "module.connecting.errorSyncModalDescription": "Please try to reconnect your device", "module.connecting.errorSyncModalHeaderTitle": "Error", @@ -333,7 +333,7 @@ "module.contacts.firstName": "First name", "module.contacts.forwardNamecard": "Forward namecard", "module.contacts.forwardTooltipDescription": "Forward namecard", - "module.contacts.googleButtonText": "Continue with Google", + "module.contacts.googleButtonText": "Log in to Google", "module.contacts.ice": "ICE", "module.contacts.iceContact": "ICE contact", "module.contacts.importBody": "Select the contacts you want to save on your Pure", @@ -356,7 +356,7 @@ "module.contacts.information": "Information", "module.contacts.listFavourites": "Favorites", "module.contacts.listUnnamedContact": "No name", - "module.contacts.manualImportText": "Import from vcf file", + "module.contacts.manualImportText": "Import vCard", "module.contacts.newContactCallLabel": "Add contact", "module.contacts.newTitle": "New contact", "module.contacts.noAddress": "No address", @@ -366,7 +366,7 @@ "module.contacts.noNotes": "No notes", "module.contacts.noPhoneNumber": "No phone number", "module.contacts.notes": "Note", - "module.contacts.outlookButtonText": "continue with outlook", + "module.contacts.outlookButtonText": "Log in to Outlook", "module.contacts.panelManageButton": "Manage", "module.contacts.panelNewContactButton": "New contact", "module.contacts.panelSearchListNoData": "No data provided", @@ -393,7 +393,8 @@ "module.contacts.speedDialTitle": "Speed dial settings", "module.contacts.synchronizingModalBody": "This might take a few minutes, please wait.", "module.contacts.synchronizingModalTitle": "Synchronising contacts…", - "module.contacts.syncModalText": "Log in to allow Mudita Center to synchronize contacts between your device and Google or iCloud accounts", + "module.contacts.syncModalHelpText": "For Apple Devices, you'll need to create a vCard (VFC file), then import it.", + "module.contacts.syncModalText": "To start importing contacts to and from your Google or Outlook accounts, simply log in to your account.", "module.contacts.syncModalTitle": "Import contacts", "module.contacts.unblock": "Unblock", "module.contacts.unblockTooltipDescription": "Unblock contact", @@ -419,11 +420,16 @@ "module.filesManager.duplicatedFilesUploadModalPendingFilesTextInfo": "Of the {uploadFilesCount} files you selected, {duplicatedFilesCount} {duplicatedFilesCount, plural, one {file} other {files}} with that name\nalready exist on your device. Change the name and try again.", "module.filesManager.duplicatedFilesUploadModalTextInfo": "The file with this name already exists on the device. Change the name and try again.", "module.filesManager.duplicatedFilesUploadModalTitle": "Upload files", + "module.filesManager.invalidFiledModalFilesInfo": "We found some files which wouldn't work on your device. To avoid problems we only uploaded the files that will work.", + "module.filesManager.invalidFiledModalHelpInfo": "To find out more about which files work on your device, visit our {link}.", + "module.filesManager.invalidFiledModalTitle": "File upload complete", + "module.filesManager.invalidFiledModalUploadInfo": "To avoid problems we only uploaded the files that will work.", "module.filesManager.panelSearchPlaceholder": "Search music files", + "module.filesManager.pendingUploadModalAbortButtonText": "Abort", "module.filesManager.pendingUploadModalActionButton": "Ok", "module.filesManager.pendingUploadModalHeader": "Files uploading", - "module.filesManager.pendingUploadModalTextDetailsInfo": "The file limit has almost been reached. You can upload only {count} {count, plural, one {file} other {files}}", - "module.filesManager.pendingUploadModalTextInfo": "Mudita Center cannot load all selected files.", + "module.filesManager.pendingUploadModalTextDetailsInfo": "The first {count, plural, one {file} other {# files}} will be uploaded to the device.", + "module.filesManager.pendingUploadModalTextInfo": "Mudita Center cannot load all files.\\nThe number of selected files exceeds the limit.", "module.filesManager.pendingUploadModalTitle": "Upload files", "module.filesManager.selectionNumber": "{num, plural, =-1 {All Files} one {# File} other {# Files}} selected", "module.filesManager.tooManyFilesTooltipDescription": "The maximum number of files has been reached ({filesSlotsHarmonyMaxLimit} files)", @@ -596,6 +602,7 @@ "module.overview.backupModalHeaderTitle": "Create backup", "module.overview.backupModalTitle": "Would you like to back up your Mudita Pure?", "module.overview.backupNoSpaceFailureModalDescription": "The device storage is full. To create a backup you need {size} of storage space. Delete unused files to allow storage space and try again.", + "module.overview.backupNoSpaceFailureWithoutDetailsModalDescription": "The device storage is almost full. To create a backup delete unused files to allow storage space and try again.", "module.overview.backupRestoreAction": "Restore last backup", "module.overview.backupRestoreBackupModalBody": "Restored files will replace the current files", "module.overview.backupRestoreBackupModalTitle": "Restore last backup", @@ -804,10 +811,15 @@ "module.settings.backupDescription": " ", "module.settings.backupLabel": "Backup Location", "module.settings.backupTetheringLabel": "Start tethering", + "module.settings.checkForFailedAppUpdateBody": "Opps, something went wrong. \nPlease check your internet connection", + "module.settings.checkForFailedAppUpdateSubtitle": "Checking failed", + "module.settings.checkForFailedAppUpdateTitle": "Mudita Center", "module.settings.collectingData": "Send Mudita Center logs to Mudita", "module.settings.collectingDataTooltip": "Sending logs is completely voluntary. Mudita doesn’t collect nor store any sensitive data - find out more in Mudita Center Privacy Policy (https://mudita.com/legal/privacy-policy/mudita-center/)", "module.settings.connection": "General", "module.settings.description": " ", + "module.settings.loadingSubtitle": "Checking for update", + "module.settings.loadingTitle": "Mudita Center", "module.settings.notifications": "Notifications", "module.settings.notificationsDescription": "Select which notifications you want to receive while using Mudita Center:", "module.settings.notificationsIncomingCallsNotificationsLabel": "Incoming calls notifications", @@ -816,6 +828,12 @@ "module.settings.notificationsPureOsUpdatesNotifications": "PureOS updates notifications", "module.settings.offLabel": "Off", "module.settings.onLabel": "On", + "module.settings.privacyPolicyModalButton": "Agree", + "module.settings.privacyPolicyModalDescription": "Please read and agree to the Privacy policy to be able to use Mudita Center.", + "module.settings.privacyPolicyModalHeader": "Read and accept the Privacy policy", + "module.settings.privacyPolicyModalLink": "Read the Privacy Policy", + "module.settings.privacyPolicyModalTitle": "Privacy Policy", + "module.settings.systemUpdateCheckFailed": "Checking for update failed", "module.settings.tetheringLabel": "Start tethering", "module.template.dropdownDelete": "Delete Template", "module.templates": "Templates", @@ -845,6 +863,7 @@ "module.templates.emptyTemplate": "Empty template", "module.templates.modalTitle": "Use Template", "module.templates.newButton": "New template", + "module.templates.newLine": "The template contains new line character", "module.templates.newTemplate": "new", "module.templates.newTitle": "New template", "module.templates.orderError": "Reordering was not successful", diff --git a/packages/app/src/__deprecated__/renderer/routes/base-routes.tsx b/packages/app/src/__deprecated__/renderer/routes/base-routes.tsx index 74cb4d3356..7f8817b8b9 100644 --- a/packages/app/src/__deprecated__/renderer/routes/base-routes.tsx +++ b/packages/app/src/__deprecated__/renderer/routes/base-routes.tsx @@ -12,7 +12,6 @@ import Music from "App/__deprecated__/renderer/modules/music/music.component" import News from "App/news/news.container" import Overview from "App/overview/overview.container" import Contacts from "App/contacts/contacts.container" -import Settings from "App/settings/settings.container" import Tethering from "App/__deprecated__/renderer/modules/tethering/tethering.container" import { URL_MAIN, @@ -94,7 +93,7 @@ export default () => ( - + ( path={`${URL_MAIN.settings}${URL_TABS.audioConversion}`} component={AudioConversionContainer} /> - { - if (env === "production") { - return createFreshdeskTicket - } else { - return mockCreateFreshdeskTicket - } + return createFreshdeskTicket })() diff --git a/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/create-freshdesk-ticket.types.ts b/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/create-freshdesk-ticket.types.ts index dbc7d257ff..a565d2cd40 100644 --- a/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/create-freshdesk-ticket.types.ts +++ b/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/create-freshdesk-ticket.types.ts @@ -10,6 +10,7 @@ export enum FreshdeskTicketDataType { export enum FreshdeskTicketProduct { Pure = "Mudita Pure", Harmony = "Mudita Harmony", + Kompakt = "Mudita Kompakt", None = "None", } diff --git a/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/map-device-type-to-product.helper.ts b/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/map-device-type-to-product.helper.ts index 7ec8992a57..b5f91f9aca 100644 --- a/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/map-device-type-to-product.helper.ts +++ b/packages/app/src/__deprecated__/renderer/utils/create-freshdesk-ticket/map-device-type-to-product.helper.ts @@ -9,6 +9,7 @@ import { FreshdeskTicketProduct } from "./create-freshdesk-ticket.types" const DEVICE_TYPE_TO_PRODUCT: Record = { [DeviceType.MuditaHarmony]: FreshdeskTicketProduct.Harmony, [DeviceType.MuditaPure]: FreshdeskTicketProduct.Pure, + [DeviceType.MuditaKompakt]: FreshdeskTicketProduct.Kompakt, } export const mapDeviceTypeToProduct = ( diff --git a/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.test.ts b/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.test.ts index f2581f1b31..ca7f398bd4 100644 --- a/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.test.ts +++ b/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.test.ts @@ -12,7 +12,6 @@ import { testQuestion, testSeedCollectionIds, } from "App/__deprecated__/seeds/help" -import { defaultHelpItems } from "App/__deprecated__/main/store/default-help-items" import { ConversionFormat, Convert } from "App/settings/constants" import { Settings } from "App/settings/dto" @@ -33,6 +32,7 @@ export const fakeAppSettings: Settings = { language: "en-US", neverConnected: true, collectingData: undefined, + privacyPolicyAccepted: false, diagnosticSentTimestamp: 0, ignoredCrashDumps: [], } @@ -100,14 +100,3 @@ describe("Online scenario", () => { expect(result.current.data.collection).toEqual(testSeedCollectionIds) }) }) - -test("returns default data when offline", async () => { - onLine.mockReturnValue(false) - const { result, waitForNextUpdate } = renderHook(() => - useHelpSearch(saveToStore, getStoreData) - ) - await waitForNextUpdate() - expect(result.current.data.collection).toHaveLength( - defaultHelpItems.collection.length - ) -}) diff --git a/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.ts b/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.ts index c5b7a0baa2..5a6635adb2 100644 --- a/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.ts +++ b/packages/app/src/__deprecated__/renderer/utils/hooks/use-help-search/use-help-search.ts @@ -8,7 +8,6 @@ import { QuestionAndAnswer } from "App/help/components/help.component" import { ipcRenderer } from "electron-better-ipc" import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" import debounce from "lodash/debounce" -import { getDefaultHelpItems } from "App/__deprecated__/main/store/default-help-items" // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -35,7 +34,8 @@ export const useHelpSearch = ( if (storeData) { setData(storeData) } else { - const defaultHelpItems = getDefaultHelpItems() + const defaultHelpItems = + require("App/help/default-help.json") as QuestionAndAnswer setData(defaultHelpItems) if (saveToStore) { await saveToStore(defaultHelpItems) diff --git a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.test.tsx b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.test.tsx index ae8e51532c..22c6e56479 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.test.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.test.tsx @@ -6,7 +6,7 @@ import { renderWithThemeAndIntl } from "App/__deprecated__/renderer/utils/render-with-theme-and-intl" import React from "react" import { fireEvent } from "@testing-library/dom" -import { screen } from "@testing-library/react" +import { screen, waitFor } from "@testing-library/react" import AppUpdateStepModal from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component" import { ModalTestIds } from "App/__deprecated__/renderer/components/core/modal/modal-test-ids.enum" import { @@ -14,6 +14,8 @@ import { AppUpdateAction, } from "App/__deprecated__/main/autoupdate" import { AppUpdateStepModalTestIds } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal-test-ids.enum" +import { Provider } from "react-redux" +import store from "../../store" const onCloseMock = jest.fn() const openExternalMock = jest.fn() @@ -60,14 +62,21 @@ jest.mock("electron", () => ({ const renderer = () => { return renderWithThemeAndIntl( - + + + ) } describe("Testing modal behavior", () => { - test("opens Update progress modal", () => { + test("opens Update progress modal", async () => { renderer() + fireEvent.click(screen.getByTestId("privacy-policy-checkbox")) + await waitFor(() => { + expect(screen.getByTestId("privacy-policy-checkbox")).toBeChecked() + expect(screen.getByTestId(ModalTestIds.ModalActionButton)).toBeEnabled() + }) fireEvent.click(screen.getByTestId(ModalTestIds.ModalActionButton)) expect( diff --git a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.tsx b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.tsx index 5d6eae0a3f..2bfc9aca62 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component.tsx @@ -4,10 +4,10 @@ */ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import React, { ComponentProps, useEffect, useState } from "react" +import React, { useEffect, useState } from "react" import { - AppUpdateForced, - AppUpdateAvailable, + AppUpdatePrivacyPolicy, + AppUpdateRejected, AppUpdateError, AppUpdateProgress, } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals" @@ -15,13 +15,13 @@ import registerDownloadedAppUpdateListener from "App/__deprecated__/main/functio import registerErrorAppUpdateListener from "App/__deprecated__/main/functions/register-error-app-update-listener" import installAppUpdateRequest from "App/__deprecated__/renderer/requests/install-app-update.request" import downloadAppUpdateRequest from "App/__deprecated__/renderer/requests/download-app-update.request" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalDialogProps } from "App/ui/components/modal-dialog" import { trackCenterUpdate, TrackCenterUpdateState, } from "App/analytic-data-tracker/helpers" -interface Props extends Partial> { +interface Props extends Partial { closeModal?: () => void appLatestVersion?: string appCurrentVersion?: string @@ -44,6 +44,11 @@ const AppUpdateStepModal: FunctionComponent = ({ const [appUpdateStep, setAppUpdateStep] = useState( AppUpdateStep.Available ) + const [appUpdateRejectedOpen, setAppUpdateRejectedOpen] = + useState(false) + const onCloseModal = () => { + setAppUpdateRejectedOpen(true) + } useEffect(() => { const unregister = registerDownloadedAppUpdateListener(() => { @@ -82,27 +87,32 @@ const AppUpdateStepModal: FunctionComponent = ({ return ( <> - {forced ? ( - ) : ( - setAppUpdateRejectedOpen(false)} appLatestVersion={appLatestVersion} {...props} /> )} - + ) diff --git a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals.tsx b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals.tsx index 4c83864461..13ecf53d3c 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals.tsx @@ -3,7 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { ComponentProps } from "react" +import React, { ComponentProps, useState } from "react" import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import { intl } from "App/__deprecated__/renderer/utils/intl" @@ -20,10 +20,18 @@ import { ModalContentWithoutMargin, RoundIconWrapper, ModalMainText, + ModalLink, } from "App/ui/components/modal-dialog" import { Size } from "App/__deprecated__/renderer/components/core/button/button.config" import { AppUpdateStepModalTestIds } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal-test-ids.enum" import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import InputCheckboxComponent from "../../components/core/input-checkbox/input-checkbox.component" +import { ipcRenderer } from "electron-better-ipc" +import { useDispatch } from "react-redux" +import { togglePrivacyPolicyAccepted } from "App/settings/actions" +import { Dispatch } from "../../store" +import styled from "styled-components" +import { BrowserActions } from "App/__deprecated__/common/enums/browser-actions.enum" export interface AppUpdateAvailableProps { appLatestVersion?: string @@ -41,39 +49,35 @@ export interface AppUpdateForcedProps { const messages = defineMessages({ appUpdateTitle: { id: "component.updateModalTitle" }, availableUpdateMessage: { id: "component.updateAvailableModalMessage" }, - availableUpdateAppVersion: { id: "component.updateAvailableModalVersion" }, + availableUpdateNoVersionMessage: { + id: "component.updateAvailableModalNoVersionMessage", + }, availableUpdateButton: { id: "component.updateAvailableModalButton" }, + availableUpdateCloseButton: { + id: "component.updateAvailableModalCloseButton", + }, availableUpdateDescription: { id: "component.updateAvailableModalDescription", }, updateForcedModalMessage: { id: "component.updateForcedModalMessage" }, - updateForcedModalVersion: { id: "component.updateForcedModalVersion" }, - updateForcedModalDescription: { - id: "component.updateForcedModalDescription", + updateForcedModalNoVersionMessage: { + id: "component.updateForcedModalNoVersionMessage", }, - updateForcedModalCurrentVersion: { - id: "component.updateForcedModalCurrentVersion", + updateForcedModalVersion: { id: "component.updateForcedModalVersion" }, + updateForcedModalPrivacyPolicy: { + id: "component.updateForcedPrivacyPolicy", }, downloadedUpdateMessage: { id: "component.updateDownloadedModalMessage" }, downloadedUpdateDescription: { id: "component.updateDownloadedModalDescription", }, - downloadedUpdateWarning: { - id: "component.updateDownloadedModalWarning", - }, downloadedUpdateButton: { id: "component.updateDownloadedModalButton" }, - downloadedUpdateCloseButton: { - id: "component.updateDownloadedModalCloseButton", - }, errorUpdateMessage: { id: "component.updateErrorModalMessage", }, errorUpdateDescription: { id: "component.updateErrorModalDescription", }, - downloadUpdateCloseButton: { - id: "component.updateDownloadCloseButton", - }, progressUpdateTitle: { id: "component.updateProgressModalTitle", }, @@ -88,6 +92,13 @@ const messages = defineMessages({ }, }) +const PrivacyPolicyCheckboxWrapper = styled.div` + display: flex; +` +const StyledLink = styled(ModalLink)` + font-size: 1.4rem; +` + const AppUpdateModal: FunctionComponent> = ({ children, testId, @@ -100,34 +111,34 @@ const AppUpdateModal: FunctionComponent> = ({ > - + {children} ) -export const AppUpdateAvailable: FunctionComponent< +export const AppUpdateRejected: FunctionComponent< ComponentProps & AppUpdateAvailableProps > = ({ appLatestVersion, ...props }) => ( - ) -export const AppUpdateForced: FunctionComponent< +export const AppUpdatePrivacyPolicy: FunctionComponent< ComponentProps & AppUpdateForcedProps -> = ({ appLatestVersion, appCurrentVersion, ...props }) => ( - - - - - - +> = ({ + appLatestVersion, + appCurrentVersion, + onActionButtonClick, + ...props +}) => { + const dispatch = useDispatch() + const [privacyPolicyCheckboxChecked, setPrivacyPolicyCheckboxChecked] = + useState(false) + const openPrivacyPolicyWindow = () => + ipcRenderer.callMain(BrowserActions.PolicyOpenBrowser) + + const onPrivacyPolicyCheckboxChange = () => { + setPrivacyPolicyCheckboxChecked(!privacyPolicyCheckboxChecked) + } + + const handleActionButtonClick = (): void => { + void dispatch(togglePrivacyPolicyAccepted(true)) + onActionButtonClick && onActionButtonClick() + } + + return ( + + + + + + + + + + + + Privacy Policy + + + + + ) +} + +export const AppUpdateError: FunctionComponent< + ComponentProps +> = (props) => { + const openMuditaWebPage = () => + ipcRenderer.callMain(BrowserActions.UpdateOpenBrowser) + return ( + - mudita.com, + }, }} - color="secondary" /> - - -) - -export const AppUpdateError: FunctionComponent< - ComponentProps -> = (props) => ( - - - - -) + + ) +} export const AppUpdateDownloaded: FunctionComponent< ComponentProps diff --git a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.stories.tsx b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.stories.tsx index d9f1349e09..ea40c1e155 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.stories.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.stories.tsx @@ -6,11 +6,11 @@ import { storiesOf } from "@storybook/react" import React from "react" import { - AppUpdateAvailable, + AppUpdateRejected, AppUpdateDownloaded, AppUpdateError, AppUpdateProgress, - AppUpdateForced, + AppUpdatePrivacyPolicy, } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals" import { ModalBackdrop, @@ -23,7 +23,7 @@ storiesOf("App/Update", module)
- +
) @@ -63,7 +63,7 @@ storiesOf("App/Update", module)
- = ({ @@ -68,6 +69,7 @@ const BaseApp: FunctionComponent = ({ osVersion, checkingForOsForceUpdate, shouldCheckForForceUpdateNeed, + initializationFailed, }) => { useRouterListener(history, { [URL_MAIN.contacts]: [], @@ -93,7 +95,12 @@ const BaseApp: FunctionComponent = ({ return } - if (deviceConnecting || deviceLocked || checkingForOsForceUpdate) { + if ( + deviceConnecting || + deviceLocked || + checkingForOsForceUpdate || + initializationFailed + ) { history.push(URL_ONBOARDING.connecting) } else if (!deviceFeaturesVisible) { history.push(URL_ONBOARDING.welcome) @@ -141,10 +148,6 @@ const isDeviceRestarting = (state: RootState & ReduxRootState): boolean => { return false } - if (state.update.updateOsState === State.Loading) { - return true - } - if (state.backup.backingUpState === State.Loading) { return true } @@ -158,6 +161,7 @@ const isDeviceRestarting = (state: RootState & ReduxRootState): boolean => { const mapStateToProps = (state: RootState & ReduxRootState) => { return { + initializationFailed: !state.dataSync.initialized, deviceFeaturesVisible: (state.device.status.connected && Boolean(state.device.status.unlocked)) || @@ -173,7 +177,8 @@ const mapStateToProps = (state: RootState & ReduxRootState) => { deviceLocked: state.device.status.connected && !state.device.status.unlocked, deviceUpdating: state.update.updateOsState === State.Loading, - deviceRestarting: isDeviceRestarting(state), + deviceRestarting: + isDeviceRestarting(state) || state.device.status.restarting, osVersion: state.device.data?.osVersion, lowestSupportedOsVersion: getDeviceLatestVersion(state), checkingForOsForceUpdate: diff --git a/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.test.tsx b/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.test.tsx index 2b396e6963..61fdb67c86 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.test.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.test.tsx @@ -19,6 +19,7 @@ import { modalsManagerReducer } from "App/modals-manager/reducers" import { settingsReducer } from "App/settings/reducers" import { checkUpdateAvailable } from "App/settings/actions/check-update-available.action" import { updateOsReducer } from "App/update/reducers" +import { dataSyncReducer } from "App/data-sync/reducers" jest.mock("App/settings/actions/check-update-available.action") @@ -109,6 +110,7 @@ const store = init({ modalsManager: modalsManagerReducer, settings: settingsReducer, update: updateOsReducer, + dataSync: dataSyncReducer, }, }, }) as Store diff --git a/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.tsx b/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.tsx index 484665d130..a8d3cb41af 100644 --- a/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.tsx +++ b/packages/app/src/__deprecated__/renderer/wrappers/root-wrapper.tsx @@ -6,7 +6,7 @@ import { DeviceType } from "App/device/constants" import { connect } from "react-redux" import { History } from "history" -import React, { useCallback, useEffect } from "react" +import React, { useCallback, useEffect, useMemo } from "react" import { IntlProvider } from "react-intl" import localeEn from "App/__deprecated__/renderer/locales/default/en-US.json" import { ThemeProvider } from "styled-components" @@ -40,6 +40,7 @@ import { setLatestVersion, toggleApplicationUpdateAvailable, checkUpdateAvailable, + setCheckingForUpdate, } from "App/settings/actions" import { registerDataSyncListener, @@ -81,6 +82,7 @@ interface Props { getCurrentDevice: () => void setConnectionStatus: (status: boolean) => void resetUploadingState: () => void + setCheckingForUpdate: (value: boolean) => void } const RootWrapper: FunctionComponent = ({ @@ -96,6 +98,7 @@ const RootWrapper: FunctionComponent = ({ connectedAndUnlocked, setConnectionStatus, resetUploadingState, + setCheckingForUpdate, }) => { const onAgreementStatusChangeListener = useCallback( (value) => { @@ -112,39 +115,42 @@ const RootWrapper: FunctionComponent = ({ const getStoreData = async (key?: string) => await ipcRenderer.callMain(HelpActions.GetStore, key) - const RenderRoutes = () => { - if (mode === Mode.ServerError) { - return - } + const RenderRoutes = useMemo( + () => () => { + if (mode === Mode.ServerError) { + return + } - if (mode === Mode.Help) { - return ( - - ) - } + if (mode === Mode.Help) { + return ( + + ) + } - if (mode === Mode.License) { - return - } + if (mode === Mode.License) { + return + } - if (mode === Mode.TermsOfService) { - return - } + if (mode === Mode.TermsOfService) { + return + } - if (mode === Mode.PrivacyPolicy) { - return - } + if (mode === Mode.PrivacyPolicy) { + return + } - if (mode === Mode.Sar) { - return - } + if (mode === Mode.Sar) { + return + } - return - } + return + }, + [mode, history] + ) const handleAppUpdateAvailableCheck = (): void => { if (!window.navigator.onLine) { @@ -218,6 +224,7 @@ const RootWrapper: FunctionComponent = ({ useEffect(() => { const unregister = registerAvailableAppUpdateListener((version) => { + setCheckingForUpdate(false) toggleApplicationUpdateAvailable(true) setLatestVersion(version as string) }) @@ -228,6 +235,7 @@ const RootWrapper: FunctionComponent = ({ useEffect(() => { const unregister = registerNotAvailableAppUpdateListener(() => { toggleApplicationUpdateAvailable(false) + setCheckingForUpdate(false) }) return () => unregister() @@ -285,6 +293,7 @@ const mapDispatchToProps = { getCurrentDevice, setConnectionStatus, resetUploadingState, + setCheckingForUpdate, } export default connect(mapStateToProps, mapDispatchToProps)(RootWrapper) diff --git a/packages/app/src/analytic-data-tracker/constants/controller.constant.ts b/packages/app/src/analytic-data-tracker/constants/controller.constant.ts index 3ff53d6bcf..1efb0ea5f4 100644 --- a/packages/app/src/analytic-data-tracker/constants/controller.constant.ts +++ b/packages/app/src/analytic-data-tracker/constants/controller.constant.ts @@ -8,13 +8,19 @@ export const ControllerPrefix = "analytic-data-tracker" export enum IpcAnalyticDataTrackerEvent { Track = "track", TrackUnique = "track-unique", + TrackWithoutDeviceCheck = "track-without-device-check", + TrackUniqueWithoutDeviceCheck = "track-unique-without-device-check", ToggleTracking = "toggle-tracking", + SetExternalUsageDevice = "set-external-usage-device", SetVisitorMetadata = "set-visitor-metadata", } export enum IpcAnalyticDataTrackerRequest { Track = "analytic-data-tracker-track", TrackUnique = "analytic-data-tracker-track-unique", + TrackWithoutDeviceCheck = "analytic-data-tracker-track-without-device-check", + TrackUniqueWithoutDeviceCheck = "analytic-data-tracker-track-unique-without-device-check", ToggleTracking = "analytic-data-tracker-toggle-tracking", + SetExternalUsageDevice = "analytic-data-tracker-set-external-usage-device", SetVisitorMetadata = "analytic-data-tracker-set-visitor-metadata", } diff --git a/packages/app/src/analytic-data-tracker/constants/track-event.constant.ts b/packages/app/src/analytic-data-tracker/constants/track-event.constant.ts index 9c5b8a7a27..9ed7b514d3 100644 --- a/packages/app/src/analytic-data-tracker/constants/track-event.constant.ts +++ b/packages/app/src/analytic-data-tracker/constants/track-event.constant.ts @@ -16,4 +16,6 @@ export enum TrackEventCategory { CenterUpdateDownload = "Center Update - download", CenterUpdateStart = "Center Update - start", CenterUpdateFail = "Center Update - fail", + PureUpdateDownload = "Pure Update - download", + HarmonyUpdateDownload = "Harmony Update - download", } diff --git a/packages/app/src/analytic-data-tracker/controllers/analytic-data-tracker.controller.ts b/packages/app/src/analytic-data-tracker/controllers/analytic-data-tracker.controller.ts index 1abb9f9665..20ad83e23c 100644 --- a/packages/app/src/analytic-data-tracker/controllers/analytic-data-tracker.controller.ts +++ b/packages/app/src/analytic-data-tracker/controllers/analytic-data-tracker.controller.ts @@ -28,11 +28,26 @@ export class AnalyticDataTrackerController { await this.tracker.trackUnique(event) } + @IpcEvent(IpcAnalyticDataTrackerEvent.TrackWithoutDeviceCheck) + public async trackWithoutDeviceCheck(event: TrackEvent): Promise { + await this.tracker.track(event, false) + } + + @IpcEvent(IpcAnalyticDataTrackerEvent.TrackUniqueWithoutDeviceCheck) + public async trackUniqueWithoutDeviceCheck(event: TrackEvent): Promise { + await this.tracker.trackUnique(event, false) + } + @IpcEvent(IpcAnalyticDataTrackerEvent.ToggleTracking) public toggleTracking(flag: boolean): void { this.tracker.toggleTracking(flag) } + @IpcEvent(IpcAnalyticDataTrackerEvent.SetExternalUsageDevice) + public setExternalUsageDevice(flag: boolean): void { + this.tracker.setExternalUsageDevice(flag) + } + @IpcEvent(IpcAnalyticDataTrackerEvent.SetVisitorMetadata) public setVisitorMetadata(visitorMetadata: VisitorMetadata): void { this.tracker.setVisitorMetadata(visitorMetadata) diff --git a/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.test.ts b/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.test.ts index f661601f38..9bd6e342f4 100644 --- a/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.test.ts +++ b/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.test.ts @@ -6,16 +6,18 @@ import { initAnalyticDataTracker } from "App/analytic-data-tracker/helpers/init-analytic-data-tracker" import { setVisitorMetadataRequest, - trackUniqueRequest, + trackUniqueWithoutDeviceCheckRequest, } from "App/analytic-data-tracker/requests" jest.mock("App/analytic-data-tracker/requests/set-visitor-metadata.request") -jest.mock("App/analytic-data-tracker/requests/track-unique.request") +jest.mock( + "App/analytic-data-tracker/requests/track-unique-without-device-check.request" +) describe("`initAnalyticDataTracker`", () => { - test("methods trigger `setVisitorMetadataRequest` and `trackUniqueRequest`", async () => { + test("methods trigger `setVisitorMetadataRequest` and `trackUniqueWithoutDeviceCheckRequest`", async () => { await initAnalyticDataTracker() expect(setVisitorMetadataRequest).toHaveBeenCalled() - expect(trackUniqueRequest).toHaveBeenCalled() + expect(trackUniqueWithoutDeviceCheckRequest).toHaveBeenCalled() }) }) diff --git a/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.ts b/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.ts index 8732a4ff51..9b9c0cb037 100644 --- a/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.ts +++ b/packages/app/src/analytic-data-tracker/helpers/init-analytic-data-tracker.ts @@ -6,7 +6,7 @@ import packageInfo from "../../../package.json" import { setVisitorMetadataRequest, - trackUniqueRequest, + trackUniqueWithoutDeviceCheckRequest, } from "App/analytic-data-tracker/requests" import { TrackEventCategory } from "App/analytic-data-tracker/constants" @@ -19,7 +19,7 @@ export const initAnalyticDataTracker = async (): Promise => { }`, }) - await trackUniqueRequest({ + await trackUniqueWithoutDeviceCheckRequest({ e_c: TrackEventCategory.CenterVersion, e_a: packageInfo.version, }) diff --git a/packages/app/src/analytic-data-tracker/helpers/track-os-download.ts b/packages/app/src/analytic-data-tracker/helpers/track-os-download.ts new file mode 100644 index 0000000000..7f2db17581 --- /dev/null +++ b/packages/app/src/analytic-data-tracker/helpers/track-os-download.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { trackRequest } from "App/analytic-data-tracker/requests" +import { TrackEvent } from "App/analytic-data-tracker/types" +import { TrackEventCategory } from "App/analytic-data-tracker/constants" +import { OsEnvironment } from "App/update/constants" +import { Product } from "App/__deprecated__/main/constants" + +export interface TrackOsDownloadOptions { + version: string + product: Product + environment: OsEnvironment + latest: boolean +} + +const getTrackEventCategory = ( + environment: OsEnvironment, + product: Product +): string | undefined => { + const productionPrefix = + environment === OsEnvironment.Production ? "" : `${environment} ` + if (product === Product.BellHybrid) { + return `${productionPrefix}${TrackEventCategory.HarmonyUpdateDownload}` + } else if (product === Product.PurePhone) { + return `${productionPrefix}${TrackEventCategory.PureUpdateDownload}` + } + + return +} + +const getTrackEventAction = (version: string, latest: boolean): string => { + if (latest) { + return `latest - ${version}` + } else { + return version + } +} + +export const trackOsDownload = async ( + options: TrackOsDownloadOptions +): Promise => { + const { version, environment, product, latest } = options + + const e_a = getTrackEventAction(version, latest) + const e_c = getTrackEventCategory(environment, product) + + if (e_c === undefined) { + return + } + + const event: TrackEvent = { + e_a, + e_c, + } + + await trackRequest(event) +} diff --git a/packages/app/src/analytic-data-tracker/helpers/track-os-update.ts b/packages/app/src/analytic-data-tracker/helpers/track-os-update.ts index 56d5026122..77e49bd039 100644 --- a/packages/app/src/analytic-data-tracker/helpers/track-os-update.ts +++ b/packages/app/src/analytic-data-tracker/helpers/track-os-update.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { trackRequest } from "App/analytic-data-tracker/requests" +import { + trackRequest, + trackWithoutDeviceCheckRequest, +} from "App/analytic-data-tracker/requests" import { HarmonyDeviceData, PureDeviceData } from "App/device" import { DeviceType } from "App/device/constants" import { TrackEvent } from "App/analytic-data-tracker/types" @@ -47,7 +50,8 @@ const getHarmonyTrackEventCategoryByState = ( } export const trackOsUpdate = async ( - options: TrackOsUpdateOptions + options: TrackOsUpdateOptions, + externalUsageDevice?: boolean ): Promise => { const { fromOsVersion, toOsVersion, state, deviceType } = options let event: TrackEvent = { @@ -69,5 +73,9 @@ export const trackOsUpdate = async ( } } - await trackRequest(event) + if (externalUsageDevice === true) { + await trackWithoutDeviceCheckRequest(event) + } else { + await trackRequest(event) + } } diff --git a/packages/app/src/analytic-data-tracker/requests/index.ts b/packages/app/src/analytic-data-tracker/requests/index.ts index 9607d77734..5521626ef9 100644 --- a/packages/app/src/analytic-data-tracker/requests/index.ts +++ b/packages/app/src/analytic-data-tracker/requests/index.ts @@ -7,3 +7,5 @@ export * from "./set-visitor-metadata.request" export * from "./toggle-tracking.request" export * from "./track.request" export * from "./track-unique.request" +export * from "./track-without-device-check.request" +export * from "./track-unique-without-device-check.request" diff --git a/packages/app/src/analytic-data-tracker/requests/set-external-usage-device.request.ts b/packages/app/src/analytic-data-tracker/requests/set-external-usage-device.request.ts new file mode 100644 index 0000000000..03bcd5717c --- /dev/null +++ b/packages/app/src/analytic-data-tracker/requests/set-external-usage-device.request.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ipcRenderer } from "electron-better-ipc" +import { IpcAnalyticDataTrackerRequest } from "App/analytic-data-tracker/constants" + +export const setExternalUsageDeviceRequest = async ( + flag: boolean +): Promise => { + return ipcRenderer.callMain( + IpcAnalyticDataTrackerRequest.SetExternalUsageDevice, + flag + ) +} diff --git a/packages/app/src/analytic-data-tracker/requests/track-unique-without-device-check.request.ts b/packages/app/src/analytic-data-tracker/requests/track-unique-without-device-check.request.ts new file mode 100644 index 0000000000..92cfe70d6c --- /dev/null +++ b/packages/app/src/analytic-data-tracker/requests/track-unique-without-device-check.request.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ipcRenderer } from "electron-better-ipc" +import { IpcAnalyticDataTrackerRequest } from "App/analytic-data-tracker/constants" +import { TrackEvent } from "App/analytic-data-tracker/types" + +export const trackUniqueWithoutDeviceCheckRequest = async ( + event: TrackEvent +): Promise => { + return ipcRenderer.callMain( + IpcAnalyticDataTrackerRequest.TrackUniqueWithoutDeviceCheck, + event + ) +} diff --git a/packages/app/src/analytic-data-tracker/requests/track-without-device-check.request.ts b/packages/app/src/analytic-data-tracker/requests/track-without-device-check.request.ts new file mode 100644 index 0000000000..bdab76cbb5 --- /dev/null +++ b/packages/app/src/analytic-data-tracker/requests/track-without-device-check.request.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ipcRenderer } from "electron-better-ipc" +import { IpcAnalyticDataTrackerRequest } from "App/analytic-data-tracker/constants" +import { TrackEvent } from "App/analytic-data-tracker/types" + +export const trackWithoutDeviceCheckRequest = async ( + event: TrackEvent +): Promise => { + return ipcRenderer.callMain( + IpcAnalyticDataTrackerRequest.TrackWithoutDeviceCheck, + event + ) +} diff --git a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker-class.interface.ts b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker-class.interface.ts index 4e04e51d00..1df1ecbb3e 100644 --- a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker-class.interface.ts +++ b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker-class.interface.ts @@ -8,8 +8,15 @@ import { VisitorMetadata } from "App/analytic-data-tracker/services/analytic-dat import { TrackEvent } from "App/analytic-data-tracker/types" export interface AnalyticDataTrackerClass { - track(event: TrackEvent): Promise - trackUnique(event: TrackEvent): Promise + track( + event: TrackEvent, + externalUsageDeviceOnly?: boolean + ): Promise + trackUnique( + event: TrackEvent, + externalUsageDeviceOnly?: boolean + ): Promise toggleTracking(flag: boolean): void + setExternalUsageDevice(flag: boolean): void setVisitorMetadata(visitorMetadata: VisitorMetadata): void } diff --git a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.factory.ts b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.factory.ts index dc3443ab06..713851cb8f 100644 --- a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.factory.ts +++ b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.factory.ts @@ -27,6 +27,9 @@ class MatomoTrackerPlaceholder implements AnalyticDataTrackerClass { toggleTracking(): void { return } + setExternalUsageDevice(): void { + return + } setVisitorMetadata(): void { return } @@ -61,7 +64,7 @@ export class AnalyticDataTrackerFactory { const appSettings = settingsService.getSettings() const _id = appSettings.applicationId - const trackingEnabled = appSettings.collectingData + const trackingEnabled = appSettings.privacyPolicyAccepted const axiosInstance: AxiosInstance = axios.create({ httpsAgent: new https.Agent({ diff --git a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.test.ts b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.test.ts index 47f6ad7db6..31883b85f6 100644 --- a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.test.ts +++ b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.test.ts @@ -45,6 +45,7 @@ describe("`AnalyticDataTrackerService`", () => { trackerCacheService, axiosInstance ) + subject.setExternalUsageDevice(true) axiosMock.onPost(apiUrl).replyOnce(200) const response = await subject.track({}) @@ -74,6 +75,8 @@ describe("`AnalyticDataTrackerService`", () => { trackerCacheService, axiosInstance ) + + subject.setExternalUsageDevice(true) axiosMock.onPost(apiUrl).replyOnce(200) const response = await subject.trackUnique({}) @@ -90,6 +93,7 @@ describe("`AnalyticDataTrackerService`", () => { axiosInstance ) + subject.setExternalUsageDevice(true) const response = await subject.trackUnique({}) expect(response).toEqual(undefined) @@ -103,6 +107,7 @@ describe("`AnalyticDataTrackerService`", () => { trackerCacheService, axiosInstance ) + subject.setExternalUsageDevice(true) axiosMock.onPost(apiUrl).reply(200) const response = await subject.track({}) expect(response).toEqual(undefined) @@ -118,6 +123,7 @@ describe("`AnalyticDataTrackerService`", () => { trackerCacheService, axiosInstance ) + subject.setExternalUsageDevice(true) axiosMock.onPost(apiUrl).reply(200) const response = await subject.track({}) expect(response).not.toEqual(undefined) @@ -128,6 +134,36 @@ describe("`AnalyticDataTrackerService`", () => { }) }) + describe("`externalUsageDevice` method", () => { + test("`setExternalUsageDevice` successfully set `externalUsageDevice` flag to `true`", async () => { + const subject = new AnalyticDataTrackerService( + { ...analyticDataTrackerOptions, trackingEnabled: true }, + trackerCacheService, + axiosInstance + ) + subject.setExternalUsageDevice(true) + axiosMock.onPost(apiUrl).reply(200) + const response = await subject.track({}) + expect(response).not.toEqual(undefined) + }) + + test("`setExternalUsageDevice` successfully set `externalUsageDevice` flag to `false`", async () => { + const subject = new AnalyticDataTrackerService( + { ...analyticDataTrackerOptions, trackingEnabled: true }, + trackerCacheService, + axiosInstance + ) + subject.setExternalUsageDevice(true) + axiosMock.onPost(apiUrl).reply(200) + const response = await subject.track({}) + expect(response).not.toEqual(undefined) + + subject.setExternalUsageDevice(false) + const response2 = await subject.track({}) + expect(response2).toEqual(undefined) + }) + }) + describe("`setVisitorMetadata` method", () => { test("`setVisitorMetadata` successfully set `visitorMetadata` field", async () => { const subject = new AnalyticDataTrackerService( @@ -135,6 +171,7 @@ describe("`AnalyticDataTrackerService`", () => { trackerCacheService, axiosInstance ) + subject.setExternalUsageDevice(true) axiosMock.onPost(apiUrl).reply(200) const response = await subject.track({}) diff --git a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.ts b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.ts index c1af45523f..f204385388 100644 --- a/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.ts +++ b/packages/app/src/analytic-data-tracker/services/analytic-data-tracker.service.ts @@ -23,6 +23,7 @@ export interface VisitorMetadata { export class AnalyticDataTrackerService implements AnalyticDataTrackerClass { private trackingEnabled: boolean + private externalUsageDevice: boolean private visitorMetadata: VisitorMetadata = {} private readonly siteId: number private readonly apiUrl: string @@ -37,22 +38,34 @@ export class AnalyticDataTrackerService implements AnalyticDataTrackerClass { this.siteId = options.siteId this.apiUrl = options.apiUrl this.trackingEnabled = options.trackingEnabled ?? true + this.externalUsageDevice = false } - public async track(event: TrackEvent): Promise { + public async track( + event: TrackEvent, + externalUsageDeviceOnly = true + ): Promise { if (!this.trackingEnabled) { return } + if (externalUsageDeviceOnly && !this.externalUsageDevice) { + return + } return this.trackRequest(event) } public async trackUnique( - event: TrackEvent + event: TrackEvent, + externalUsageDeviceOnly = true ): Promise { if (!this.trackingEnabled) { return } + if (externalUsageDeviceOnly && !this.externalUsageDevice) { + return + } + if (!(await this.trackerCacheService.isEventUnique(event))) { return } @@ -70,6 +83,10 @@ export class AnalyticDataTrackerService implements AnalyticDataTrackerClass { this.trackingEnabled = flag } + public setExternalUsageDevice(flag: boolean): void { + this.externalUsageDevice = flag + } + public setVisitorMetadata(visitorMetadata: VisitorMetadata): void { this.visitorMetadata = visitorMetadata } diff --git a/packages/app/src/backup/actions/start-backup-device.action.ts b/packages/app/src/backup/actions/start-backup-device.action.ts index f0129fe251..70ecc3f303 100644 --- a/packages/app/src/backup/actions/start-backup-device.action.ts +++ b/packages/app/src/backup/actions/start-backup-device.action.ts @@ -9,12 +9,16 @@ import { CreateBackup } from "App/backup/dto" import { createBackupRequest } from "App/backup/requests/create-backup.request" import { loadBackupData } from "App/backup/actions/load-backup-data.action" import { AppError } from "App/core/errors" -import { ReduxRootState, RootState } from "App/__deprecated__/renderer/store" +import { ReduxRootState } from "App/__deprecated__/renderer/store" -export const startBackupDevice = createAsyncThunk( +export const startBackupDevice = createAsyncThunk< + undefined, + CreateBackup, + { state: ReduxRootState } +>( BackupEvent.CreateBackup, async ({ key }, { getState, dispatch, rejectWithValue }) => { - const state = getState() as RootState & ReduxRootState + const state = getState() const pureOsBackupDesktopFileDir = state.settings.osBackupLocation if ( @@ -34,7 +38,6 @@ export const startBackupDevice = createAsyncThunk( .replace(/\./g, "") .replace(/-/g, "") .replace(/:/g, "")}` - const downloadDeviceBackupResponse = await createBackupRequest({ key, fileBase, diff --git a/packages/app/src/backup/actions/start-restore-device.action.ts b/packages/app/src/backup/actions/start-restore-device.action.ts index 68016491f9..8d45f35dc7 100644 --- a/packages/app/src/backup/actions/start-restore-device.action.ts +++ b/packages/app/src/backup/actions/start-restore-device.action.ts @@ -8,13 +8,17 @@ import { BackupError, BackupEvent } from "App/backup/constants" import { RestoreBackup } from "App/backup/dto" import { restoreBackupRequest } from "App/backup/requests" import { AppError } from "App/core/errors" -import { PureDeviceData } from "App/device" -import { ReduxRootState, RootState } from "App/__deprecated__/renderer/store" +import { ReduxRootState } from "App/__deprecated__/renderer/store" +import { PureDeviceData } from "App/device/reducers/device.interface" -export const startRestoreDevice = createAsyncThunk( +export const startRestoreDevice = createAsyncThunk< + undefined, + RestoreBackup, + { state: ReduxRootState } +>( BackupEvent.RestoreBackup, async ({ key, backup }, { getState, rejectWithValue }) => { - const state = getState() as RootState & ReduxRootState + const state = getState() const osBackupFilePath = (state.device.data as PureDeviceData | undefined) ?.backupFilePath @@ -26,7 +30,6 @@ export const startRestoreDevice = createAsyncThunk( ) ) } - const result = await restoreBackupRequest({ token: key, filePath: backup.filePath, diff --git a/packages/app/src/connecting/components/connecting-content.component.tsx b/packages/app/src/connecting/components/connecting-content.component.tsx index 0a8af84ebc..dbb2f05735 100644 --- a/packages/app/src/connecting/components/connecting-content.component.tsx +++ b/packages/app/src/connecting/components/connecting-content.component.tsx @@ -13,6 +13,8 @@ import Loader from "App/__deprecated__/renderer/components/core/loader/loader.co import { LoaderType } from "App/__deprecated__/renderer/components/core/loader/loader.interface" import styled from "styled-components" import { backgroundColor } from "App/__deprecated__/renderer/styles/theming/theme-getters" +import { useSelector } from "react-redux" +import { ReduxRootState } from "App/__deprecated__/renderer/store" export const Container = styled.section` display: grid; @@ -47,6 +49,7 @@ interface Props { } const ConnectingContent: FunctionComponent = ({ longerConnection }) => { + const { initialized } = useSelector((state: ReduxRootState) => state.dataSync) return (
@@ -56,9 +59,10 @@ const ConnectingContent: FunctionComponent = ({ longerConnection }) => {
diff --git a/packages/app/src/connecting/components/connecting.component.tsx b/packages/app/src/connecting/components/connecting.component.tsx index 3c635615ef..6deb6396ea 100644 --- a/packages/app/src/connecting/components/connecting.component.tsx +++ b/packages/app/src/connecting/components/connecting.component.tsx @@ -21,6 +21,7 @@ import ErrorSyncModal from "App/connecting/components/error-sync-modal/error-syn import { ConnectingError } from "App/connecting/components/connecting-error.enum" import { AppError } from "App/core/errors" import CriticalBatteryLevelModal from "App/connecting/components/critical-battery-level-modal/critical-battery-level-modal" +import ErrorUpdateModal from "App/connecting/components/error-update-modal/error-update-modal" const Connecting: FunctionComponent<{ loaded: boolean @@ -164,7 +165,7 @@ const Connecting: FunctionComponent<{ )} {error === ConnectingError.ForceUpdateCheckFailed && ( - + )} ) => { ...defaultProps, ...extraProps, } - const outcome = renderWithThemeAndIntl() + const outcome = renderWithThemeAndIntl( + + + + ) return { ...outcome, } diff --git a/packages/app/src/connecting/components/critical-battery-level-modal/critical-battery-level-modal.tsx b/packages/app/src/connecting/components/critical-battery-level-modal/critical-battery-level-modal.tsx index eeb5266954..32f96a8deb 100644 --- a/packages/app/src/connecting/components/critical-battery-level-modal/critical-battery-level-modal.tsx +++ b/packages/app/src/connecting/components/critical-battery-level-modal/critical-battery-level-modal.tsx @@ -52,7 +52,6 @@ const CriticalBatteryLevelModal: FunctionComponent< closeButtonLabel={intl.formatMessage( messages.criticalBatteryLevelModalButton )} - zIndex={100} {...props} > diff --git a/packages/app/src/connecting/components/error-connecting-modal.tsx b/packages/app/src/connecting/components/error-connecting-modal.tsx index c274c064f9..d2386bb462 100644 --- a/packages/app/src/connecting/components/error-connecting-modal.tsx +++ b/packages/app/src/connecting/components/error-connecting-modal.tsx @@ -4,19 +4,30 @@ */ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { intl } from "App/__deprecated__/renderer/utils/intl" -import { RoundIconWrapper } from "App/__deprecated__/renderer/components/core/modal-shared/modal-shared" +import { intl, textFormatters } from "App/__deprecated__/renderer/utils/intl" import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" -import { ModalText } from "App/contacts/components/sync-contacts-modal/sync-contacts.styled" -import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" +import Text, { + TextDisplayStyle, +} from "App/__deprecated__/renderer/components/core/text/text.component" import React, { ComponentProps } from "react" import { defineMessages } from "react-intl" import styled from "styled-components" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { + ModalContent, + ModalDialog, + RoundIconWrapper, +} from "App/ui/components/modal-dialog" import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" import { Size } from "App/__deprecated__/renderer/components/core/button/button.config" import { ErrorConnectingModalTestIds } from "App/connecting/components/error-connecting-modal-test-ids.enum" import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" +import { + fontWeight, + textColor, +} from "App/__deprecated__/renderer/styles/theming/theme-getters" +import { ipcRenderer } from "electron-better-ipc" +import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" const messages = defineMessages({ errorConnectingModalHeaderTitle: { @@ -32,20 +43,23 @@ const messages = defineMessages({ id: "module.connecting.errorConnectingDescription", }, }) - -const ModalContent = styled.div` - display: flex; - flex-direction: column; - align-items: center; - - p:first-of-type { - margin-top: 0; +const StyledLink = styled.a` + text-decoration: underline; + cursor: pointer; + font-size: 1.4rem; + font-weight: ${fontWeight("default")}; + color: ${textColor("action")}; +` +const StyledModalContent = styled(ModalContent)` + p { + text-align: left; } ` const ErrorConnectingModal: FunctionComponent< ComponentProps > = ({ onClose, ...props }) => { + const openHelpWindow = () => ipcRenderer.callMain(HelpActions.OpenWindow) return ( - + - - + connection help page. + + ), + }, + }} /> - + ) } diff --git a/packages/app/src/connecting/components/error-sync-modal/error-sync-modal.tsx b/packages/app/src/connecting/components/error-sync-modal/error-sync-modal.tsx index 053c249243..2f1964076a 100644 --- a/packages/app/src/connecting/components/error-sync-modal/error-sync-modal.tsx +++ b/packages/app/src/connecting/components/error-sync-modal/error-sync-modal.tsx @@ -9,10 +9,10 @@ import { RoundIconWrapper } from "App/__deprecated__/renderer/components/core/mo import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" import { ModalText } from "App/contacts/components/sync-contacts-modal/sync-contacts.styled" import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" -import React, { ComponentProps } from "react" +import React from "react" import { defineMessages } from "react-intl" import styled from "styled-components" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalDialog, ModalDialogProps } from "App/ui/components/modal-dialog" import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" import { Size } from "App/__deprecated__/renderer/components/core/button/button.config" import { ErrorSyncModalTestIds } from "App/connecting/components/error-sync-modal/error-sync-modal-test-ids.enum" @@ -42,7 +42,7 @@ const ModalContent = styled.div` margin-top: 0; } ` -interface Props extends ComponentProps { +interface Props extends ModalDialogProps { onRetry: () => void } @@ -56,7 +56,6 @@ const ErrorSyncModal: FunctionComponent = ({ onRetry, ...props }) => { actionButtonLabel={intl.formatMessage(messages.errorSyncModalButton)} onActionButtonClick={onRetry} closeButton={false} - zIndex={100} {...props} > diff --git a/packages/app/src/connecting/components/error-update-modal/error-update-modal-test-ids.enum.tsx b/packages/app/src/connecting/components/error-update-modal/error-update-modal-test-ids.enum.tsx new file mode 100644 index 0000000000..e04077bcda --- /dev/null +++ b/packages/app/src/connecting/components/error-update-modal/error-update-modal-test-ids.enum.tsx @@ -0,0 +1,8 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum ErrorUpdateModalTestIds { + Container = "error-update-modal-container", +} diff --git a/packages/app/src/connecting/components/error-update-modal/error-update-modal.stories.tsx b/packages/app/src/connecting/components/error-update-modal/error-update-modal.stories.tsx new file mode 100644 index 0000000000..4bdd101e52 --- /dev/null +++ b/packages/app/src/connecting/components/error-update-modal/error-update-modal.stories.tsx @@ -0,0 +1,23 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React from "react" +import { Meta } from "@storybook/react" +import Story from "App/__deprecated__/renderer/components/storybook/story.component" +import ErrorUpdateModal from "App/connecting/components/error-update-modal/error-update-modal" +import { noop } from "App/__deprecated__/renderer/utils/noop" + +export const ErrorUpdateModalStory = (): JSX.Element => { + return ( + + + + ) +} + +export default { + title: "Views|Connecting/Backup Modal Dialogs", + component: ErrorUpdateModalStory, +} as Meta diff --git a/packages/app/src/connecting/components/error-update-modal/error-update-modal.tsx b/packages/app/src/connecting/components/error-update-modal/error-update-modal.tsx new file mode 100644 index 0000000000..9454a12cea --- /dev/null +++ b/packages/app/src/connecting/components/error-update-modal/error-update-modal.tsx @@ -0,0 +1,73 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import { RoundIconWrapper } from "App/__deprecated__/renderer/components/core/modal-shared/modal-shared" +import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" +import { ModalText } from "App/contacts/components/sync-contacts-modal/sync-contacts.styled" +import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" +import React, { ComponentProps } from "react" +import { defineMessages } from "react-intl" +import styled from "styled-components" +import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" +import { Size } from "App/__deprecated__/renderer/components/core/button/button.config" +import { ErrorUpdateModalTestIds } from "App/connecting/components/error-update-modal/error-update-modal-test-ids.enum" +import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" + +const messages = defineMessages({ + errorUpdateModalHeaderTitle: { + id: "module.connecting.errorUpdateModalHeaderTitle", + }, + errorUpdateModalTitle: { + id: "module.connecting.errorUpdateModalTitle", + }, + errorUpdateModalDescription: { + id: "module.connecting.errorUpdateModalDescription", + }, +}) + +const ModalContent = styled.div` + display: flex; + flex-direction: column; + align-items: center; + + p:first-of-type { + margin-top: 0; + } +` + +const ErrorUpdateModal: FunctionComponent< + ComponentProps +> = ({ onClose, ...props }) => { + return ( + + + + + + + + + + ) +} + +export default ErrorUpdateModal diff --git a/packages/app/src/contact-support/components/contact-support-flow.component.tsx b/packages/app/src/contact-support/components/contact-support-flow.component.tsx index 7125d7b517..586d89f17d 100644 --- a/packages/app/src/contact-support/components/contact-support-flow.component.tsx +++ b/packages/app/src/contact-support/components/contact-support-flow.component.tsx @@ -11,15 +11,14 @@ import ContactSupportModalError from "App/contact-support/components/contact-sup import { ContactSupportFlowTestIds } from "App/contact-support/components/contact-support-flow-test-ids.component" import { SendTicketState } from "App/contact-support/reducers" import { SendTicketPayload } from "App/contact-support/actions/send-ticket.action" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" interface Props - extends Pick< - ComponentProps, - "files" - > { + extends Pick, "files"> { state: SendTicketState | null sendTicket: (payload: SendTicketPayload) => void closeContactSupportFlow: () => void + layer?: ModalLayers } const ContactSupportFlow: FunctionComponent = ({ @@ -27,10 +26,12 @@ const ContactSupportFlow: FunctionComponent = ({ files, sendTicket, closeContactSupportFlow, + layer = ModalLayers.ContactSupport, }) => { return ( <> = ({ files={files} /> = ({
= ({ /> ( return rejectWithValue( new AppError( ContactsEvent.AddNewContact, - error?.message ?? "Add Contact request failed" + error?.message ?? "Add Contact request failed", + error?.data ) ) } diff --git a/packages/app/src/contacts/components/contacts/contacts.component.test.tsx b/packages/app/src/contacts/components/contacts/contacts.component.test.tsx index 3f6ea2da49..495641c510 100644 --- a/packages/app/src/contacts/components/contacts/contacts.component.test.tsx +++ b/packages/app/src/contacts/components/contacts/contacts.component.test.tsx @@ -30,10 +30,6 @@ window.IntersectionObserver = jest .fn() .mockImplementation(intersectionObserverMock) -window.IntersectionObserver = jest - .fn() - .mockImplementation(intersectionObserverMock) - type Props = ComponentProps jest.mock("electron", () => ({ diff --git a/packages/app/src/contacts/components/contacts/contacts.component.tsx b/packages/app/src/contacts/components/contacts/contacts.component.tsx index 7a28fd0af6..0f6edd454a 100644 --- a/packages/app/src/contacts/components/contacts/contacts.component.tsx +++ b/packages/app/src/contacts/components/contacts/contacts.component.tsx @@ -56,6 +56,26 @@ import { ExportContactFailedModal } from "../export-contact-failed-modal/export- import { applyValidationRulesToImportedContacts } from "App/contacts/helpers/apply-validation-rules-to-imported-contacts/apply-validation-rules-to-imported-contacts" import { ExportContactsResult } from "App/contacts/constants" import DeleteContactsPopup from "./delete-contacts-popup/delete-contacts-popup.component" +import { differenceWith, isEqual } from "lodash" + +const allPossibleFormErrorCausedByAPI: FormError[] = [ + { + field: "primaryPhoneNumber", + error: "component.formErrorNumberUnique", + }, + { + field: "secondaryPhoneNumber", + error: "component.formErrorNumberUnique", + }, + { + field: "primaryPhoneNumber", + error: "component.formErrorRequiredPrimaryPhone", + }, + { + field: "primaryPhoneNumber", + error: "component.formErrorNumberUnique", + }, +] export const messages = defineMessages({ deleteTitle: { id: "module.contacts.deleteTitle" }, @@ -171,23 +191,49 @@ const Contacts: FunctionComponent = ({ true ) - const { payload } = await delayResponse(addNewContact(contact)) + const { message, payload } = + (await delayResponse(addNewContact(contact))).payload ?? {} - if (payload) { - let newError: FormError - if (payload.message === "Create contact: Empty primary phone number") { - newError = { + if (payload || message) { + const newError: FormError[] = [] + if ( + message === "phone-number-duplicated" && + payload?.primaryPhoneNumberIsDuplicated + ) { + newError.push({ + field: "primaryPhoneNumber", + error: "component.formErrorNumberUnique", + }) + } + if ( + message === "phone-number-duplicated" && + payload?.secondaryPhoneNumberIsDuplicated + ) { + newError.push({ + field: "secondaryPhoneNumber", + error: "component.formErrorNumberUnique", + }) + } + if (message === "Create contact: Empty primary phone number") { + newError.push({ field: "primaryPhoneNumber", error: "component.formErrorRequiredPrimaryPhone", - } - } else { - newError = { + }) + } + if (newError.length === 0) { + newError.push({ field: "primaryPhoneNumber", error: "component.formErrorNumberUnique", - } + }) } - setFormErrors([...formErrors, newError]) + const cleanedErrors = differenceWith( + formErrors, + allPossibleFormErrorCausedByAPI, + (a, b) => isEqual(a, b) + ) + + setFormErrors([...cleanedErrors, ...newError]) await closeModal() return } diff --git a/packages/app/src/contacts/components/contacts/contacts.interface.ts b/packages/app/src/contacts/components/contacts/contacts.interface.ts index 55f50c817b..3cea81d903 100644 --- a/packages/app/src/contacts/components/contacts/contacts.interface.ts +++ b/packages/app/src/contacts/components/contacts/contacts.interface.ts @@ -55,7 +55,7 @@ export interface ContactsProps { authorize: (provider: ExternalProvider) => Promise> addNewContact: ( contact: NewContact - ) => Promise> + ) => Promise> importContact: ( contact: NewContact ) => Promise> @@ -86,4 +86,8 @@ export type FormError = { field: keyof Contact; error: string } export interface ContactErrorResponse { status: RequestResponseStatus message: string + payload?: { + primaryPhoneNumberIsDuplicated?: boolean + secondaryPhoneNumberIsDuplicated?: boolean + } } diff --git a/packages/app/src/contacts/components/sync-contacts-modal/sync-contacts-modal.component.tsx b/packages/app/src/contacts/components/sync-contacts-modal/sync-contacts-modal.component.tsx index 2fbae9bba3..19043689c6 100644 --- a/packages/app/src/contacts/components/sync-contacts-modal/sync-contacts-modal.component.tsx +++ b/packages/app/src/contacts/components/sync-contacts-modal/sync-contacts-modal.component.tsx @@ -3,24 +3,27 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { useRef, ComponentProps } from "react" +import React, { useRef, ComponentProps, ReactNode } from "react" import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import { noop } from "App/__deprecated__/renderer/utils/noop" import { intl } from "App/__deprecated__/renderer/utils/intl" -import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" +import Text, { + TextDisplayStyle, +} from "App/__deprecated__/renderer/components/core/text/text.component" import { ButtonsContainer, ButtonWrapper, - ModalText, SyncButton, } from "App/contacts/components/sync-contacts-modal/sync-contacts.styled" import { SyncContactsModalTestIds } from "App/contacts/components/sync-contacts-modal/sync-contacts-modal-test-ids.enum" import { defineMessages } from "react-intl" import GoogleButton from "react-google-button" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalDialog, ModalLink } from "App/ui/components/modal-dialog" import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" import InputFileSelect from "App/contacts/components/sync-contacts-modal/input-file-select" +import { ipcRenderer } from "electron-better-ipc" +import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" const messages = defineMessages({ title: { @@ -29,6 +32,9 @@ const messages = defineMessages({ text: { id: "module.contacts.syncModalText", }, + helpText: { + id: "module.contacts.syncModalHelpText", + }, googleButtonText: { id: "module.contacts.googleButtonText", }, @@ -70,6 +76,8 @@ const SyncContactsModal: FunctionComponent = ({ } } + const openHelpWindow = () => ipcRenderer.callMain(HelpActions.OpenWindow) + return ( = ({ onClose={onClose} {...props} > - + ( + {chunks} + ), + }, + }} + /> boolean = contactTypeGuard ): PhoneContacts => { if (guard(data)) { diff --git a/packages/app/src/contacts/reducers/contacts.interface.ts b/packages/app/src/contacts/reducers/contacts.interface.ts index a565a8e908..e5679c21a6 100644 --- a/packages/app/src/contacts/reducers/contacts.interface.ts +++ b/packages/app/src/contacts/reducers/contacts.interface.ts @@ -7,27 +7,8 @@ import { PayloadAction } from "@reduxjs/toolkit" import { ContactsEvent } from "App/contacts/constants" export type ContactID = string -export type Contact = - | ContactWithPhoneNumber - | ContactWithEmail - | ContactWithFirstName - | ContactWithLastName -// AUTO DISABLED - fix me if you like :) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ContactFactorySignature = (...args: any[]) => T -export type NewContact = Omit -export type ContactsState = PhoneContacts & - Pick & { - error: Error | string | null - selectedItems: { - rows: string[] - allItemsSelected: boolean - } - deletedCount: number - } -export type Store = StoreData & StoreSelectors -export interface BaseContactModel { +export interface Contact { id: ContactID firstName?: string lastName?: string @@ -43,25 +24,20 @@ export interface BaseContactModel { secondAddressLine?: string } -export interface ContactWithID extends BaseContactModel { - id: ContactID -} - -export interface ContactWithPhoneNumber extends ContactWithID { - primaryPhoneNumber: string -} - -export interface ContactWithEmail extends ContactWithID { - email: string -} - -export interface ContactWithFirstName extends ContactWithID { - firstName: string -} - -export interface ContactWithLastName extends ContactWithID { - lastName: string -} +// AUTO DISABLED - fix me if you like :) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type ContactFactorySignature = (...args: any[]) => T +export type NewContact = Omit +export type ContactsState = PhoneContacts & + Pick & { + error: Error | string | null + selectedItems: { + rows: string[] + allItemsSelected: boolean + } + deletedCount: number + } +export type Store = StoreData & StoreSelectors export interface PhoneContacts { collection: ContactID[] diff --git a/packages/app/src/contacts/reducers/contacts.reducer.ts b/packages/app/src/contacts/reducers/contacts.reducer.ts index bc0de5e699..e247e147f4 100644 --- a/packages/app/src/contacts/reducers/contacts.reducer.ts +++ b/packages/app/src/contacts/reducers/contacts.reducer.ts @@ -60,10 +60,20 @@ export const contactsReducer = createReducer( const contacts = Object.keys(action.payload.contacts).map( (key) => action.payload.contacts[key] ) - + const contactsDb = contactDatabaseFactory(contacts) + const rows = state.selectedItems.rows.filter((row) => + contactsDb.collection.includes(row) + ) + const selectedItems = { + rows, + allItemsSelected: contactsDb.collection.every((row) => + rows.includes(row) + ), + } return { ...state, - ...contactDatabaseFactory(contacts), + ...contactsDb, + selectedItems, resultState: ResultState.Loaded, error: null, } diff --git a/packages/app/src/contacts/services/contact.service.ts b/packages/app/src/contacts/services/contact.service.ts index 5779cdf1ec..40ea8c3f86 100644 --- a/packages/app/src/contacts/services/contact.service.ts +++ b/packages/app/src/contacts/services/contact.service.ts @@ -8,6 +8,7 @@ import { GetContactResponseBody, GetContactsResponseBody, CreateContactResponseBody, + CreateContactErrorResponseBody, } from "App/device/types/mudita-os" import { DeviceManager } from "App/device-manager/services" import { Contact, ContactID } from "App/contacts/reducers" @@ -107,6 +108,30 @@ export class ContactService { status: RequestResponseStatus.Ok, data: contact, } + // error type cannot be typed correctly, response method needs enhancement + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + } else if (response.error?.payload?.status === "phone-number-duplicated") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const errorPayloadData = (response.error?.payload?.data ?? { + duplicateNumbers: [], + }) as CreateContactErrorResponseBody + + return { + status: RequestResponseStatus.Error, + error: { + message: "phone-number-duplicated", + data: { + primaryPhoneNumberIsDuplicated: + errorPayloadData.duplicateNumbers.includes( + newContact.primaryPhoneNumber + ), + secondaryPhoneNumberIsDuplicated: + errorPayloadData.duplicateNumbers.includes( + newContact.secondaryPhoneNumber ?? "" + ), + }, + }, + } } else { return { status: RequestResponseStatus.Error, diff --git a/packages/app/src/core/application.module.ts b/packages/app/src/core/application.module.ts index 71aee43ab7..5398d90fd9 100644 --- a/packages/app/src/core/application.module.ts +++ b/packages/app/src/core/application.module.ts @@ -56,7 +56,6 @@ export class ApplicationModule { ContactModule, MessageModule, FilesManagerModule, - CrashDumpModule, TemplateModule, SearchModule, UpdateModule, @@ -66,7 +65,7 @@ export class ApplicationModule { DeviceModule, DeviceManagerModule, ] - public lateModules: Module[] = [DataSyncModule] + public lateModules: Module[] = [DataSyncModule, CrashDumpModule] private deviceLogger: DeviceLogger = LoggerFactory.getInstance() private index = new IndexFactory().create() @@ -87,11 +86,7 @@ export class ApplicationModule { ) constructor(private ipc: MainProcessIpc) { - const enabled = - process.env.NODE_ENV === "development" && - process.env.DISABLE_DEV_DEVICE_LOGGER === "1" - ? false - : flags.get(Feature.LoggerEnabled) + const enabled = flags.get(Feature.LoggerEnabled) this.deviceLogger.registerLogger(new PureLogger()) this.deviceLogger.toggleLogs(enabled) diff --git a/packages/app/src/crash-dump/components/crash-dump-modal/crash-dump-modal.component.tsx b/packages/app/src/crash-dump/components/crash-dump-modal/crash-dump-modal.component.tsx index a039278da4..63ba54848d 100644 --- a/packages/app/src/crash-dump/components/crash-dump-modal/crash-dump-modal.component.tsx +++ b/packages/app/src/crash-dump/components/crash-dump-modal/crash-dump-modal.component.tsx @@ -8,7 +8,7 @@ import { defineMessages } from "react-intl" import { DeviceType } from "App/device/constants" import { intl } from "App/__deprecated__/renderer/utils/intl" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { ModalDialog } from "App/ui/components/modal-dialog" +import { ModalDialog, ModalDialogProps } from "App/ui/components/modal-dialog" import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" import Text, { @@ -36,7 +36,7 @@ import { } from "App/__deprecated__/renderer/components/core/button/button.config" import { noop } from "App/__deprecated__/renderer/utils/noop" -export interface CrashDumpProps { +export interface CrashDumpProps extends Omit { open: boolean deviceType: DeviceType onClose: () => void @@ -68,6 +68,7 @@ export const CrashDumpModal: FunctionComponent = ({ deviceType, onClose, onSubmit = noop, + ...rest }) => { const { register, @@ -93,6 +94,7 @@ export const CrashDumpModal: FunctionComponent = ({ open={open} closeModal={handleCloseModal} closeButton={false} + {...rest} > diff --git a/packages/app/src/crash-dump/containers/crash-dump/crash-dump.component.tsx b/packages/app/src/crash-dump/containers/crash-dump/crash-dump.component.tsx index 97c9a40366..53d74c76a9 100644 --- a/packages/app/src/crash-dump/containers/crash-dump/crash-dump.component.tsx +++ b/packages/app/src/crash-dump/containers/crash-dump/crash-dump.component.tsx @@ -22,6 +22,7 @@ import { } from "App/crash-dump/actions" import { ReduxRootState } from "App/__deprecated__/renderer/store" import { intl } from "App/__deprecated__/renderer/utils/intl" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" const messages = defineMessages({ crashDumpModalErrorTitle: { @@ -53,6 +54,7 @@ const CrashDumpContainer: FunctionComponent = ({ downloadCrashDump, ignoreCrashDump, resetCrashDump, + layer = ModalLayers.CrashDump, }) => { const [openInfo, setOpenInfo] = useState(false) @@ -92,6 +94,7 @@ const CrashDumpContainer: FunctionComponent = ({ <> {failed && ( = ({ )} {sending && ( = ({ )} {sended && ( = ({ )} {openInfo && ( void ignoreCrashDump: () => void diff --git a/packages/app/src/data-sync/observers/device-connection.observer.ts b/packages/app/src/data-sync/observers/device-connection.observer.ts index a6a853100d..bb38235005 100644 --- a/packages/app/src/data-sync/observers/device-connection.observer.ts +++ b/packages/app/src/data-sync/observers/device-connection.observer.ts @@ -74,7 +74,7 @@ export class DeviceConnectionObserver implements Observer { const { serialNumber, deviceToken, version } = data as GetDeviceInfoResponseBody - const baseVersion = version.split("-")[0] + const baseVersion = String(version).split("-")[0] if (corruptedPureOSVersions.some((v) => v === baseVersion)) { this.ipc.sendToRenderers(IpcEvent.DataSkipped) diff --git a/packages/app/src/data-sync/presenters/thread/thread.presenter.ts b/packages/app/src/data-sync/presenters/thread/thread.presenter.ts index 56f4dee0d8..1fef27629c 100644 --- a/packages/app/src/data-sync/presenters/thread/thread.presenter.ts +++ b/packages/app/src/data-sync/presenters/thread/thread.presenter.ts @@ -90,7 +90,7 @@ export class ThreadPresenter { contactName: contact ? [contact?.name_primary, contact?.name_alternative].join(" ") : "", - phoneNumber: contactNumber?.number_user, + phoneNumber: contactNumber?.number_user?.replace(/[\s]/g, ""), lastUpdatedAt: new Date(Number(thread.date) * 1000), messageSnippet: sms ? this.buildMessageSnippet(sms) : "", unread: Number(thread.read) !== 0, diff --git a/packages/app/src/device-file-system/commands/file-upload.command.test.ts b/packages/app/src/device-file-system/commands/file-upload.command.test.ts index 069a4ae76d..1d2fd3c630 100644 --- a/packages/app/src/device-file-system/commands/file-upload.command.test.ts +++ b/packages/app/src/device-file-system/commands/file-upload.command.test.ts @@ -27,6 +27,7 @@ const deviceManager = { const fileSystemService = { readFile: jest.fn(), + getFileSize: jest.fn(), } as unknown as FileSystemService const subject = new FileUploadCommand(deviceManager, fileSystemService) diff --git a/packages/app/src/device-file-system/commands/file-upload.command.ts b/packages/app/src/device-file-system/commands/file-upload.command.ts index 7df33f1808..86edee0d5c 100644 --- a/packages/app/src/device-file-system/commands/file-upload.command.ts +++ b/packages/app/src/device-file-system/commands/file-upload.command.ts @@ -27,10 +27,19 @@ export class FileUploadCommand extends BaseCommand { filePath: string ): Promise> { let data: Buffer | Uint8Array - + const maxFileSize = 2000000000 try { + const fileSize = await this.fileSystemService.getFileSize(filePath) + if (fileSize >= maxFileSize) { + return Result.failed( + new AppError( + DeviceFileSystemError.UnsupportedFileSize, + `Uploading file: file ${filePath} is too large` + ) + ) + } data = await this.fileSystemService.readFile(filePath) - } catch (error) { + } catch (err) { return Result.failed( new AppError( DeviceFileSystemError.FileUploadUnreadable, @@ -102,6 +111,9 @@ export class FileUploadCommand extends BaseCommand { chunkNo, data: chunkedBuffer.toString("base64"), }, + options: { + connectionTimeOut: 5000, + }, }) if (!response.ok) { diff --git a/packages/app/src/device-file-system/constants/error.enum.ts b/packages/app/src/device-file-system/constants/error.enum.ts index 8b9ac6e72e..dcd8998259 100644 --- a/packages/app/src/device-file-system/constants/error.enum.ts +++ b/packages/app/src/device-file-system/constants/error.enum.ts @@ -11,4 +11,5 @@ export enum DeviceFileSystemError { FilesRetrieve = "DEVICE_FILES_RETRIEVE_ERROR", FileDeleteCommand = "DEVICE_FILE_DELETE_ERROR", NoSpaceLeft = "NO_SPACE_LEFT_ON_DEVICE_ERROR", + UnsupportedFileSize = "UNSUPPORTED_FILE_SIZE_ERROR", } diff --git a/packages/app/src/device-manager/actions/get-current-device.action.ts b/packages/app/src/device-manager/actions/get-current-device.action.ts index e625b532ad..71fcbc281a 100644 --- a/packages/app/src/device-manager/actions/get-current-device.action.ts +++ b/packages/app/src/device-manager/actions/get-current-device.action.ts @@ -8,18 +8,20 @@ import { connectDevice } from "App/device/actions" import { DeviceManagerEvent } from "App/device-manager/constants" import { readAllIndexes, setDataSyncInitialized } from "App/data-sync/actions" import { getCurrentDeviceRequest } from "App/device-manager/requests" +import { ReduxRootState } from "App/__deprecated__/renderer/store" -export const getCurrentDevice = createAsyncThunk( - DeviceManagerEvent.GetCurrentDevice, - async (_, { dispatch }) => { - const { ok, data } = await getCurrentDeviceRequest() +export const getCurrentDevice = createAsyncThunk< + void, + void, + { state: ReduxRootState } +>(DeviceManagerEvent.GetCurrentDevice, async (_, { dispatch }) => { + const { ok, data } = await getCurrentDeviceRequest() - if (!ok || !data) { - return - } - - void dispatch(connectDevice(data.deviceType)) - void dispatch(readAllIndexes()) - dispatch(setDataSyncInitialized()) + if (!ok || !data) { + return } -) + + void dispatch(connectDevice(data.deviceType)) + void dispatch(readAllIndexes()) + dispatch(setDataSyncInitialized()) +}) diff --git a/packages/app/src/device-manager/device-manager.module.ts b/packages/app/src/device-manager/device-manager.module.ts index 054c80035c..4176c74323 100644 --- a/packages/app/src/device-manager/device-manager.module.ts +++ b/packages/app/src/device-manager/device-manager.module.ts @@ -15,7 +15,6 @@ import { UsbDeviceAttachObserver, DeviceDisconnectedObserver, } from "App/device-manager/observers" -import { ConnectedDeviceInitializer } from "App/device-manager/initializers" import { DeviceManagerController } from "App/device-manager/controllers" import { DeviceInitializationFailedObserver } from "App/device-manager/observers/device-initialization-failed.observer" @@ -51,15 +50,12 @@ export class DeviceManagerModule extends BaseModule { this.deviceManager, this.eventEmitter ) - const connectedDeviceInitializer = new ConnectedDeviceInitializer( - this.deviceManager, - this.ipc - ) + const deviceManagerController = new DeviceManagerController( this.deviceManager ) - this.initializers = [connectedDeviceInitializer] + this.initializers = [] this.observers = [ usbDeviceAttachObserver, deviceDisconnectedObserver, diff --git a/packages/app/src/device-manager/initializers/connected-devices.initializer.ts b/packages/app/src/device-manager/initializers/connected-devices.initializer.ts deleted file mode 100644 index 5d2bb9ce35..0000000000 --- a/packages/app/src/device-manager/initializers/connected-devices.initializer.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { MainProcessIpc } from "electron-better-ipc" -import { Initializer } from "App/core/types" -import { DeviceManager } from "App/device-manager/services" - -export class ConnectedDeviceInitializer implements Initializer { - constructor( - private deviceManager: DeviceManager, - private ipc: MainProcessIpc - ) {} - - public async initialize(): Promise { - const devices = await this.deviceManager.getConnectedDevices() - - devices.forEach((device) => { - void this.deviceManager.addDevice(device) - }) - } -} diff --git a/packages/app/src/device-manager/observers/usb-device-attach.observer.ts b/packages/app/src/device-manager/observers/usb-device-attach.observer.ts index 29354765da..bc97bbdca0 100644 --- a/packages/app/src/device-manager/observers/usb-device-attach.observer.ts +++ b/packages/app/src/device-manager/observers/usb-device-attach.observer.ts @@ -3,28 +3,37 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import usb from "usb" import { Observer } from "App/core/types" -import { PortInfoValidator } from "App/device-manager/validators" import { DeviceManager } from "App/device-manager/services" +import { PortInfoValidator } from "App/device-manager/validators" +const intervalTime = 3000 export class UsbDeviceAttachObserver implements Observer { constructor(private deviceManager: DeviceManager) {} public observe(): void { - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-misused-promises - usb.on("attach", async (device) => { - const { idVendor, idProduct } = device.deviceDescriptor + void this.watchConnectedDevices() + } + + private async watchConnectedDevices(): Promise { + const devices = await this.deviceManager.getConnectedDevices() + devices.forEach((device) => { const portInfo = { - vendorId: idVendor.toString(16).padStart(4, "0"), - productId: idProduct.toString(16).padStart(4, "0"), + vendorId: device.vendorId, + productId: device.productId, } - if (PortInfoValidator.isVendorIdValid(portInfo)) { - await this.deviceManager.addDevice(portInfo) + void this.deviceManager.addDevice(device) } }) + + return new Promise((resolve) => { + setTimeout(() => { + void (async () => { + resolve(await this.watchConnectedDevices()) + })() + }, intervalTime) + }) } } diff --git a/packages/app/src/device-manager/services/device-manager.service.ts b/packages/app/src/device-manager/services/device-manager.service.ts index ce5c2af841..bb4bacec07 100644 --- a/packages/app/src/device-manager/services/device-manager.service.ts +++ b/packages/app/src/device-manager/services/device-manager.service.ts @@ -15,6 +15,8 @@ import { PortInfoValidator } from "App/device-manager/validators" import { ListenerEvent, DeviceManagerError } from "App/device-manager/constants" import { DeviceServiceEvent } from "App/device" import { EventEmitter } from "events" +import logger from "App/__deprecated__/main/utils/logger" +import { Mutex } from "async-mutex" export class DeviceManager { public currentDevice: Device | undefined @@ -49,7 +51,19 @@ export class DeviceManager { return Array.from(this.devicesMap.values()) } + private mutex = new Mutex() + public async addDevice(port: PortInfo): Promise { + await this.mutex.runExclusive(async () => { + await this.addDeviceTaks(port) + }) + } + + public async addDeviceTaks(port: PortInfo): Promise { + if (this.currentDevice) { + return + } + const device = await this.initializeDevice(port) if (!device) { @@ -70,6 +84,7 @@ export class DeviceManager { } this.ipc.sendToRenderers(ListenerEvent.DeviceAttached, device) + logger.info(`Connected device with serial number: ${device.serialNumber}`) } public removeDevice(path: string): void { @@ -89,6 +104,7 @@ export class DeviceManager { } this.ipc.sendToRenderers(ListenerEvent.DeviceDetached, path) + logger.info(`Disconnected device with path: ${path}`) } public setCurrentDevice(path: string): ResultObject { @@ -115,7 +131,6 @@ export class DeviceManager { public async getConnectedDevices(): Promise { const portList = await this.getSerialPortList() - return ( portList // AUTO DISABLED - fix me if you like :) @@ -130,18 +145,26 @@ export class DeviceManager { const sleep = () => new Promise((resolve) => setTimeout(resolve, 500)) const retryLimit = 20 + portInfo.productId = portInfo.productId?.toUpperCase() + portInfo.vendorId = portInfo.vendorId?.toUpperCase() + + const alreadyInitializedDevices = Array.from(this.devicesMap.keys()) + // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor return new Promise(async (resolve) => { for (let i = 0; i < retryLimit; i++) { const portList = await this.getConnectedDevices() const port = portList.find( - ({ productId, vendorId }) => - productId === portInfo.productId && vendorId === portInfo.vendorId + ({ productId, vendorId, path }) => + productId?.toUpperCase() === portInfo.productId && + vendorId?.toUpperCase() === portInfo.vendorId && + ((!portInfo.path && !alreadyInitializedDevices.includes(path)) || + path === portInfo.path) ) if (port) { - const device = this.deviceResolver.resolve(portInfo, port.path) + const device = this.deviceResolver.resolve(port) if (!device) { return diff --git a/packages/app/src/device-manager/services/device-resolver.service.test.ts b/packages/app/src/device-manager/services/device-resolver.service.test.ts index c3fe63b911..10a6fcf28b 100644 --- a/packages/app/src/device-manager/services/device-resolver.service.test.ts +++ b/packages/app/src/device-manager/services/device-resolver.service.test.ts @@ -12,16 +12,12 @@ const eventEmitter = new EventEmitter() const subject = new DeviceResolverService(ipcMain, eventEmitter) describe("Pure descriptor", () => { - test.each([ - ProductID.MuditaPure, - ProductID.MuditaPureNotMtpTemporary, - ProductID.MuditaPureTemporary, - ])( + test.each([ProductID.MuditaPure])( "returns Device with MuditaPure device type if %s productID has been provided", (productId) => { - expect(subject.resolve({ productId }, "/dev/123")?.deviceType).toEqual( - DeviceType.MuditaPure - ) + expect( + subject.resolve({ productId, path: "/dev/123" })?.deviceType + ).toEqual(DeviceType.MuditaPure) } ) }) @@ -29,14 +25,39 @@ describe("Pure descriptor", () => { describe("Harmony descriptor", () => { test("returns Device with MuditaPure device type if 0300 productID has been provided", () => { expect( - subject.resolve({ productId: ProductID.MuditaHarmony }, "/dev/123") + subject.resolve({ productId: ProductID.MuditaHarmony, path: "/dev/123" }) ?.deviceType ).toEqual(DeviceType.MuditaHarmony) }) }) +describe("Kompakt descriptor", () => { + test("returns Device with MuditaKompakt device type productID has been provided", () => { + expect( + subject.resolve({ + productId: ProductID.MuditaKompaktChargeDec, + path: "/dev/123", + })?.deviceType + ).toEqual(DeviceType.MuditaKompakt) + expect( + subject.resolve({ + productId: ProductID.MuditaKompaktNoDebugDec, + path: "/dev/123", + })?.deviceType + ).toEqual(DeviceType.MuditaKompakt) + expect( + subject.resolve({ + productId: ProductID.MuditaKompaktTransferDec, + path: "/dev/123", + })?.deviceType + ).toEqual(DeviceType.MuditaKompakt) + }) +}) + describe("Unknown descriptor", () => { test("returns undefined if unknown product id has been provided", () => { - expect(subject.resolve({ productId: "0000" }, "/dev/123")).toBeUndefined() + expect( + subject.resolve({ productId: "0000", path: "/dev/123" }) + ).toBeUndefined() }) }) diff --git a/packages/app/src/device-manager/services/device-resolver.service.ts b/packages/app/src/device-manager/services/device-resolver.service.ts index 540cb310bb..2422490977 100644 --- a/packages/app/src/device-manager/services/device-resolver.service.ts +++ b/packages/app/src/device-manager/services/device-resolver.service.ts @@ -9,32 +9,42 @@ import { EventEmitter } from "events" import { MuditaPureDescriptor, MuditaHarmonyDescriptor, + MuditaKompaktDescriptor, } from "App/device/descriptors" import { Device } from "App/device/modules/device" import { DeviceFactory } from "App/device/factories" export class DeviceResolverService { - private eligibleDevices = [MuditaPureDescriptor, MuditaHarmonyDescriptor] + private eligibleDevices = [ + MuditaPureDescriptor, + MuditaHarmonyDescriptor, + MuditaKompaktDescriptor, + ] constructor( private ipc: MainProcessIpc, private eventEmitter: EventEmitter ) {} - public resolve( - portInfo: Pick, - path: string - ): Device | undefined { - const id = portInfo.productId?.toLowerCase() ?? "" + public resolve({ + productId, + serialNumber, + path, + }: Pick): + | Device + | undefined { + const id = productId?.toLowerCase() ?? "" const descriptor = this.eligibleDevices.find((device) => - device.productIds.map((item) => item.toLowerCase()).includes(id) + device.productIds + .map((item) => item.toString().toLowerCase()) + .includes(id) ) if (!descriptor) { return } - return DeviceFactory.create( + const newDevice = DeviceFactory.create( path, descriptor.deviceType, descriptor.adapter, @@ -42,5 +52,9 @@ export class DeviceResolverService { this.ipc, this.eventEmitter ) + + newDevice.serialNumber = serialNumber ?? "" + + return newDevice } } diff --git a/packages/app/src/device-manager/types/port.type.ts b/packages/app/src/device-manager/types/port.type.ts index a2941df488..70f90e04af 100644 --- a/packages/app/src/device-manager/types/port.type.ts +++ b/packages/app/src/device-manager/types/port.type.ts @@ -5,4 +5,4 @@ import { PortInfo as SerialPortInfo } from "serialport" -export type PortInfo = Omit +export type PortInfo = Partial diff --git a/packages/app/src/device-manager/validators/port-info.validator.test.ts b/packages/app/src/device-manager/validators/port-info.validator.test.ts index 1b1cfde667..6733cc73f6 100644 --- a/packages/app/src/device-manager/validators/port-info.validator.test.ts +++ b/packages/app/src/device-manager/validators/port-info.validator.test.ts @@ -23,6 +23,12 @@ test("isPortInfoMatch function works properly", () => { expect( PortInfoValidator.isPortInfoMatch({ vendorId: "3310", productId: "0100" }) ).toBeTruthy() + expect( + PortInfoValidator.isPortInfoMatch({ vendorId: "3310", productId: "AAAA" }) + ).toBeFalsy() + expect( + PortInfoValidator.isPortInfoMatch({ vendorId: "bbbb", productId: "0100" }) + ).toBeFalsy() expect(PortInfoValidator.isPortInfoMatch({ vendorId: "3310" })).toBeFalsy() expect(PortInfoValidator.isPortInfoMatch({ productId: "0100" })).toBeFalsy() expect(PortInfoValidator.isPortInfoMatch({})).toBeFalsy() diff --git a/packages/app/src/device-manager/validators/port-info.validator.ts b/packages/app/src/device-manager/validators/port-info.validator.ts index 1c57ff5d66..3f1add55f5 100644 --- a/packages/app/src/device-manager/validators/port-info.validator.ts +++ b/packages/app/src/device-manager/validators/port-info.validator.ts @@ -7,33 +7,58 @@ import { PortInfo } from "serialport" import { MuditaPureDescriptor, MuditaHarmonyDescriptor, + MuditaKompaktDescriptor, } from "../../device/descriptors" export class PortInfoValidator { - static eligibleDevices = [MuditaPureDescriptor, MuditaHarmonyDescriptor] + static eligibleDevices = [ + MuditaPureDescriptor, + MuditaHarmonyDescriptor, + MuditaKompaktDescriptor, + ] static isVendorIdValid(portInfo: Partial): boolean { const id = portInfo.vendorId?.toLowerCase() ?? "" - return Boolean( - PortInfoValidator.eligibleDevices.find((device) => - device.vendorIds.map((item) => item.toLowerCase()).includes(id) - ) + + const result = Boolean( + PortInfoValidator.eligibleDevices.find((device) => { + const vendorIds = device.vendorIds.map((item) => + item.toString().toLowerCase() + ) + + return vendorIds.includes(id) + }) ) + + return result } static isProductIdValid(portInfo: Partial): boolean { const id = portInfo.productId?.toLowerCase() ?? "" return Boolean( PortInfoValidator.eligibleDevices.find((device) => - device.productIds.map((item) => item.toLowerCase()).includes(id) + device.productIds + .map((item) => item.toString().toLowerCase()) + .includes(id) ) ) } static isPortInfoMatch(portInfo: Partial): boolean { - return ( - PortInfoValidator.isVendorIdValid(portInfo) && - PortInfoValidator.isProductIdValid(portInfo) + const vendorId = portInfo.vendorId?.toLowerCase() ?? "" + const productId = portInfo.productId?.toLowerCase() ?? "" + + return Boolean( + PortInfoValidator.eligibleDevices.find(({ vendorIds, productIds }) => { + return ( + vendorIds + .map((item) => item.toString().toLowerCase()) + .includes(vendorId) && + productIds + .map((item) => item.toString().toLowerCase()) + .includes(productId) + ) + }) ) } } diff --git a/packages/app/src/device/actions/base.action.ts b/packages/app/src/device/actions/base.action.ts index 6fd1c971fc..025cd88613 100644 --- a/packages/app/src/device/actions/base.action.ts +++ b/packages/app/src/device/actions/base.action.ts @@ -15,6 +15,7 @@ import { GetPhoneLockTimeResponseBody } from "App/device/types/mudita-os" export const setDeviceData = createAction< Partial >(DeviceEvent.SetData) + export const setLockTime = createAction< GetPhoneLockTimeResponseBody | undefined >(DeviceEvent.SetLockTime) @@ -26,7 +27,15 @@ export const setInitState = createAction(DeviceEvent.SetInitState) export const setAgreementStatus = createAction( DeviceEvent.AgreementStatus ) -export const unlockedDevice = createAction(DeviceEvent.Unlocked) +export const unlockedDevice = createAction( + DeviceEvent.Unlocked +) export const setCriticalBatteryLevel = createAction( DeviceEvent.CriticalBatteryLevel ) + +export const setExternalUsageDevice = createAction( + DeviceEvent.ExternalUsageDevice +) + +export const setRestarting = createAction(DeviceEvent.Restarting) diff --git a/packages/app/src/device/actions/connect-device.action.ts b/packages/app/src/device/actions/connect-device.action.ts index 54515d3529..73a8a07fa7 100644 --- a/packages/app/src/device/actions/connect-device.action.ts +++ b/packages/app/src/device/actions/connect-device.action.ts @@ -16,10 +16,12 @@ import { DeviceEvent, } from "App/device/constants" import { connectDeviceRequest } from "App/device/requests" +import { ReduxRootState } from "App/__deprecated__/renderer/store" export const connectDevice = createAsyncThunk< DeviceType | undefined, - DeviceType + DeviceType, + { state: ReduxRootState } >(DeviceEvent.Connected, async (payload, { dispatch, rejectWithValue }) => { const data = await connectDeviceRequest() diff --git a/packages/app/src/device/actions/disconnect-device.action.test.ts b/packages/app/src/device/actions/disconnect-device.action.test.ts index 78262ad835..75af653e0c 100644 --- a/packages/app/src/device/actions/disconnect-device.action.test.ts +++ b/packages/app/src/device/actions/disconnect-device.action.test.ts @@ -8,16 +8,9 @@ import thunk from "redux-thunk" import { AnyAction } from "@reduxjs/toolkit" import { pendingAction } from "App/__deprecated__/renderer/store/helpers" import { disconnectDevice } from "App/device/actions/disconnect-device.action" -import { disconnectDeviceRequest } from "App/device/requests/disconnect-device.request" -import { testError } from "App/__deprecated__/renderer/store/constants" -import { AppError } from "App/core/errors" -import { Result } from "App/core/builder" -import { DeviceError } from "App/device/constants" const mockStore = createMockStore([thunk])() -jest.mock("App/device/requests/disconnect-device.request") - jest.mock("App/device/actions/set-connection-status.action", () => ({ setConnectionStatus: jest.fn().mockReturnValue({ type: pendingAction("DEVICE_SET_CONNECTION_STATE"), @@ -31,9 +24,6 @@ afterEach(() => { describe("Disconnect Device request returns `success` status", () => { test("fire async `disconnectDevice`", async () => { - ;(disconnectDeviceRequest as jest.Mock).mockReturnValueOnce( - Result.success(true) - ) const { meta: { requestId }, // AUTO DISABLED - fix me if you like :) @@ -48,29 +38,5 @@ describe("Disconnect Device request returns `success` status", () => { }, disconnectDevice.fulfilled(undefined, requestId, undefined), ]) - - expect(disconnectDeviceRequest).toHaveBeenCalled() - }) -}) - -describe("Disconnect Device request returns `error` status", () => { - test("fire async `disconnectDevice` action and execute `rejected` event", async () => { - ;(disconnectDeviceRequest as jest.Mock).mockReturnValueOnce( - Result.failed(new AppError("", "")) - ) - const errorMock = new AppError( - DeviceError.Disconnection, - "Cannot disconnect from device" - ) - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await mockStore.dispatch(disconnectDevice() as unknown as AnyAction) - - expect(mockStore.getActions()).toEqual([ - disconnectDevice.pending(requestId), - disconnectDevice.rejected(testError, requestId, undefined, errorMock), - ]) }) }) diff --git a/packages/app/src/device/actions/disconnect-device.action.ts b/packages/app/src/device/actions/disconnect-device.action.ts index 55ef5eb0a5..0e261649c8 100644 --- a/packages/app/src/device/actions/disconnect-device.action.ts +++ b/packages/app/src/device/actions/disconnect-device.action.ts @@ -4,28 +4,14 @@ */ import { createAsyncThunk } from "@reduxjs/toolkit" -import { AppError } from "App/core/errors" import { setConnectionStatus } from "App/device/actions/set-connection-status.action" -import { DeviceError, DeviceEvent } from "App/device/constants" -import { disconnectDeviceRequest } from "App/device/requests" +import { DeviceEvent } from "App/device/constants" export const disconnectDevice = createAsyncThunk( DeviceEvent.Disconnected, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (_, { dispatch, rejectWithValue }) => { - const response = await disconnectDeviceRequest() - - if (!response.ok || !response.data) { - return rejectWithValue( - new AppError( - DeviceError.Disconnection, - "Cannot disconnect from device", - response - ) - ) - } - + (_, { dispatch }) => { void dispatch(setConnectionStatus(false)) return diff --git a/packages/app/src/device/actions/load-device-data.action.test.ts b/packages/app/src/device/actions/load-device-data.action.test.ts index 692671ef66..eec6e1a22a 100644 --- a/packages/app/src/device/actions/load-device-data.action.test.ts +++ b/packages/app/src/device/actions/load-device-data.action.test.ts @@ -8,16 +8,27 @@ import thunk from "redux-thunk" import { Result } from "App/core/builder" import { AnyAction } from "@reduxjs/toolkit" import { ConnectionState, DeviceError } from "App/device/constants" -import { loadDeviceData } from "App/device/actions" +import { loadDeviceData, setExternalUsageDevice } from "App/device/actions" import { PureDeviceData, HarmonyDeviceData } from "App/device/reducers" import { testError } from "App/__deprecated__/renderer/store/constants" import { AppError } from "App/core/errors" import { getDeviceInfoRequest } from "App/device-info/requests" +import { externalUsageDevice } from "App/device/requests/external-usage-device.request" +import { setExternalUsageDeviceRequest } from "App/analytic-data-tracker/requests/set-external-usage-device.request" jest.mock("App/device-info/requests") +jest.mock("App/device/requests/external-usage-device.request") +jest.mock( + "App/analytic-data-tracker/requests/set-external-usage-device.request" +) const errorMock = new AppError(DeviceError.Loading, "Device data loading error") +beforeEach(() => { + ;(externalUsageDevice as jest.Mock).mockResolvedValue(true) + ;(setExternalUsageDeviceRequest as jest.Mock).mockResolvedValue(undefined) +}) + afterEach(() => { jest.clearAllMocks() }) @@ -72,6 +83,9 @@ describe("Device type: MuditaPure", () => { device: { state: ConnectionState.Empty, }, + settings: { + privacyPolicyAccepted: true, + }, }) const { meta: { requestId }, @@ -81,6 +95,7 @@ describe("Device type: MuditaPure", () => { expect(mockStore.getActions()).toEqual([ loadDeviceData.pending(requestId), + setExternalUsageDevice(true), { type: "DEVICE_SET_DATA", payload: { @@ -122,6 +137,9 @@ describe("Device type: MuditaPure", () => { device: { state: ConnectionState.Empty, }, + settings: { + privacyPolicyAccepted: true, + }, }) const { meta: { requestId }, @@ -161,6 +179,9 @@ describe("Device type: MuditaHarmony", () => { device: { state: ConnectionState.Empty, }, + settings: { + privacyPolicyAccepted: true, + }, }) const { meta: { requestId }, @@ -170,6 +191,7 @@ describe("Device type: MuditaHarmony", () => { expect(mockStore.getActions()).toEqual([ loadDeviceData.pending(requestId), + setExternalUsageDevice(true), { type: "DEVICE_SET_DATA", payload: { @@ -200,6 +222,9 @@ describe("Device type: MuditaHarmony", () => { device: { state: ConnectionState.Empty, }, + settings: { + privacyPolicyAccepted: true, + }, }) const { meta: { requestId }, diff --git a/packages/app/src/device/actions/load-device-data.action.ts b/packages/app/src/device/actions/load-device-data.action.ts index 269a093ce1..c6b2374102 100644 --- a/packages/app/src/device/actions/load-device-data.action.ts +++ b/packages/app/src/device/actions/load-device-data.action.ts @@ -10,52 +10,73 @@ import { DeviceCommunicationError, } from "App/device/constants" import { ReduxRootState } from "App/__deprecated__/renderer/store" -import { setDeviceData } from "App/device/actions/base.action" +import { + setDeviceData, + setExternalUsageDevice, +} from "App/device/actions/base.action" import { lockedDevice } from "App/device/actions/locked-device.action" import { getDeviceInfoRequest } from "App/device-info/requests" import { setValue, MetadataKey } from "App/metadata" import { trackOsVersion } from "App/analytic-data-tracker/helpers" +import { externalUsageDevice } from "App/device/requests/external-usage-device.request" +import { setExternalUsageDeviceRequest } from "App/analytic-data-tracker/requests/set-external-usage-device.request" + +export const loadDeviceData = createAsyncThunk< + undefined, + void, + { state: ReduxRootState } +>(DeviceEvent.Loading, async (_, { getState, dispatch, rejectWithValue }) => { + const state = getState() + + if (state.device.state === ConnectionState.Loaded) { + return + } + + try { + const { ok, data, error } = await getDeviceInfoRequest() -export const loadDeviceData = createAsyncThunk( - DeviceEvent.Loading, - async (_, { getState, dispatch, rejectWithValue }) => { - const state = getState() as ReduxRootState + if (!ok || !data) { + if (error?.type === DeviceCommunicationError.DeviceLocked) { + void dispatch(lockedDevice()) + } - if (state.device.state === ConnectionState.Loaded) { return } - try { - const { ok, data, error } = await getDeviceInfoRequest() + if (state.device.deviceType !== null) { + void trackOsVersion({ + serialNumber: data.serialNumber, + osVersion: data.osVersion, + deviceType: state.device.deviceType, + }) + } - if (!ok || !data) { - if (error?.type === DeviceCommunicationError.DeviceLocked) { - void dispatch(lockedDevice()) - } + void setValue({ + key: MetadataKey.DeviceOsVersion, + value: data.osVersion ?? null, + }) + void setValue({ + key: MetadataKey.DeviceType, + value: state.device.deviceType, + }) - return - } + if ( + data.serialNumber !== state.device.data?.serialNumber || + state.device.externalUsageDevice === null + ) { + const resultExternalUsageDevice = state.settings.privacyPolicyAccepted + ? await externalUsageDevice(data.serialNumber) + : false - if (state.device.deviceType !== null) { - void trackOsVersion({ - serialNumber: data.serialNumber, - osVersion: data.osVersion, - deviceType: state.device.deviceType, - }) + await setExternalUsageDeviceRequest(resultExternalUsageDevice) + if (state.settings.privacyPolicyAccepted !== undefined) { + dispatch(setExternalUsageDevice(resultExternalUsageDevice)) } - void setValue({ - key: MetadataKey.DeviceOsVersion, - value: data.osVersion ?? null, - }) - void setValue({ - key: MetadataKey.DeviceType, - value: state.device.deviceType, - }) - dispatch(setDeviceData(data)) - } catch (error) { - return rejectWithValue(error) } - - return + dispatch(setDeviceData(data)) + } catch (error) { + return rejectWithValue(error) } -) + + return +}) diff --git a/packages/app/src/device/actions/unlock-device.action.ts b/packages/app/src/device/actions/unlock-device.action.ts index 5f53afe289..482dfe2364 100644 --- a/packages/app/src/device/actions/unlock-device.action.ts +++ b/packages/app/src/device/actions/unlock-device.action.ts @@ -12,27 +12,28 @@ import { AppError } from "App/core/errors" // DEPRECATED import { ReduxRootState } from "App/__deprecated__/renderer/store" -export const unlockDevice = createAsyncThunk( - DeviceEvent.Unlock, - async (code, { rejectWithValue, dispatch, getState }) => { - const data = await unlockDeviceRequest(code) +export const unlockDevice = createAsyncThunk< + boolean, + number[], + { state: ReduxRootState } +>(DeviceEvent.Unlock, async (code, { rejectWithValue, dispatch, getState }) => { + const data = await unlockDeviceRequest(code) - const state = getState() as ReduxRootState + const state = getState() - if (!data.ok) { - return rejectWithValue( - new AppError( - DeviceError.Unlocking, - "Something went wrong during unlocking", - data - ) + if (!data.ok) { + return rejectWithValue( + new AppError( + DeviceError.Unlocking, + "Something went wrong during unlocking", + data ) - } + ) + } - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - void dispatch(connectDevice(state.device.deviceType!)) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + void dispatch(connectDevice(state.device.deviceType!)) - return Boolean(data.data) - } -) + return Boolean(data.data) +}) diff --git a/packages/app/src/device/constants/battery-state-kompakt.constant.ts b/packages/app/src/device/constants/battery-state-kompakt.constant.ts new file mode 100644 index 0000000000..e83428b346 --- /dev/null +++ b/packages/app/src/device/constants/battery-state-kompakt.constant.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum BatteryStateKompakt { + UNKNOWN = 1, + CHARGING = 2, + DISCHARGING = 3, + NOT_CHARGING = 4, + FULL = 5, +} diff --git a/packages/app/src/device/constants/controller.constant.ts b/packages/app/src/device/constants/controller.constant.ts index 0b43e49deb..b2bed9f3fe 100644 --- a/packages/app/src/device/constants/controller.constant.ts +++ b/packages/app/src/device/constants/controller.constant.ts @@ -7,7 +7,6 @@ export const ControllerPrefix = "device" export enum IpcDeviceEvent { Connect = "connect", - Disconnect = "disconnect", Unlock = "unlock", UnlockStatus = "unlock-status", LockTime = "lock-time", @@ -16,7 +15,6 @@ export enum IpcDeviceEvent { export enum IpcDeviceRequest { Connect = "device-connect", - Disconnect = "device-disconnect", Unlock = "device-unlock", UnlockStatus = "device-unlock-status", LockTime = "device-lock-time", diff --git a/packages/app/src/device/constants/device-type.constant.ts b/packages/app/src/device/constants/device-type.constant.ts index e0d7e57b50..05529515ef 100644 --- a/packages/app/src/device/constants/device-type.constant.ts +++ b/packages/app/src/device/constants/device-type.constant.ts @@ -6,4 +6,5 @@ export enum DeviceType { MuditaPure = "MuditaPure", MuditaHarmony = "MuditaHarmony", + MuditaKompakt = "MuditaKompakt", } diff --git a/packages/app/src/settings/components/collecting-data-modal/index.ts b/packages/app/src/device/constants/endpoint-kompakt.constant.ts similarity index 75% rename from packages/app/src/settings/components/collecting-data-modal/index.ts rename to packages/app/src/device/constants/endpoint-kompakt.constant.ts index 2756acfee6..64330c9bfb 100644 --- a/packages/app/src/settings/components/collecting-data-modal/index.ts +++ b/packages/app/src/device/constants/endpoint-kompakt.constant.ts @@ -3,4 +3,6 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export * from "./collecting-data-modal.container" +export enum EndpointKompakt { + DeviceInfo = 1, +} diff --git a/packages/app/src/device/constants/event.enum.ts b/packages/app/src/device/constants/event.enum.ts index c391c15531..4097ee2555 100644 --- a/packages/app/src/device/constants/event.enum.ts +++ b/packages/app/src/device/constants/event.enum.ts @@ -29,4 +29,8 @@ export enum DeviceEvent { LoadStorageInfo = "DEVICE_LOAD_STORAGE_INFO", AgreementStatus = "DEVICE_AGREEMENT_STATUS", CriticalBatteryLevel = "CRITICAL_BATTERY_LEVEL", + + ExternalUsageDevice = "EXTERNAL_USAGE_DEVICE", + + Restarting = "DEVICE_RESTARTING", } diff --git a/packages/app/src/device/constants/external-data-api-event.constant.ts b/packages/app/src/device/constants/external-data-api-event.constant.ts new file mode 100644 index 0000000000..3cb04bb8dd --- /dev/null +++ b/packages/app/src/device/constants/external-data-api-event.constant.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum ExternalDataApiEvent { + GetExternalUsageDevice = "get-external-usage-device", +} diff --git a/packages/app/src/device/constants/index.ts b/packages/app/src/device/constants/index.ts index b3e58c796f..ffb7a88f23 100644 --- a/packages/app/src/device/constants/index.ts +++ b/packages/app/src/device/constants/index.ts @@ -37,3 +37,8 @@ export * from "./restore-state.constant" export * from "./signal-strength.constant" export * from "./sim-card.constant" export * from "./tray.constant" +export * from "./method-kompakt.constant" +export * from "./endpoint-kompakt.constant" +export * from "./battery-state-kompakt.constant" +export * from "./network-status-kompakt.constant" +export * from "./prefix-kompakt.constant" diff --git a/packages/app/src/device/constants/method-kompakt.constant.ts b/packages/app/src/device/constants/method-kompakt.constant.ts new file mode 100644 index 0000000000..427000bb39 --- /dev/null +++ b/packages/app/src/device/constants/method-kompakt.constant.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum MethodKompakt { + GET = 1, + POST = 2, + PUT = 3, + DELETE = 4, +} diff --git a/packages/app/src/device/constants/network-status-kompakt.constant.ts b/packages/app/src/device/constants/network-status-kompakt.constant.ts new file mode 100644 index 0000000000..0e0968c37a --- /dev/null +++ b/packages/app/src/device/constants/network-status-kompakt.constant.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum NetworkStatusKompakt { + IN_SERVICE = 0, + OUT_OF_SERVICE = 1, + EMERGENCY_ONLY = 2, + POWER_OFF = 3, + UNKNOWN = 4, +} diff --git a/packages/app/src/device-manager/initializers/index.ts b/packages/app/src/device/constants/prefix-kompakt.constant.ts similarity index 69% rename from packages/app/src/device-manager/initializers/index.ts rename to packages/app/src/device/constants/prefix-kompakt.constant.ts index bbb303dd91..cc67c4b314 100644 --- a/packages/app/src/device-manager/initializers/index.ts +++ b/packages/app/src/device/constants/prefix-kompakt.constant.ts @@ -3,4 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export * from "./connected-devices.initializer" +export enum PrefixKompakt { + REQUEST = "?", + RESPONSE = "@", +} diff --git a/packages/app/src/device/constants/product-identifier.constant.ts b/packages/app/src/device/constants/product-identifier.constant.ts index 11d0ae8d7f..1130f63632 100644 --- a/packages/app/src/device/constants/product-identifier.constant.ts +++ b/packages/app/src/device/constants/product-identifier.constant.ts @@ -6,16 +6,17 @@ export enum ProductID { MuditaPure = "0100", MuditaHarmony = "0300", - - // TODO: Remove tmp values when pure will be released, https://appnroll.atlassian.net/browse/CP-457 - MuditaPureNotMtpTemporary = "0102", - MuditaPureTemporary = "0622", + MuditaKompaktChargeHex = "2006", //0x2006 + MuditaKompaktTransferHex = "200a", //0x200a + MuditaKompaktNoDebugHex = "2012", //0x2012 + MuditaKompaktChargeDec = "8198", //0x2006 + MuditaKompaktTransferDec = "8202", //0x200a + MuditaKompaktNoDebugDec = "8210", //0x2012 } export enum VendorID { MuditaPure = "3310", MuditaHarmony = "3310", - - // TODO: Remove tmp values when pure will be released, https://appnroll.atlassian.net/browse/CP-457 - MuditaPureTemporary = "045e", + MuditaKompaktHex = "0e8d", + MuditaKompaktDec = "3725", } diff --git a/packages/app/src/device/constants/response-status.constant.ts b/packages/app/src/device/constants/response-status.constant.ts index 58613e94c0..c428a5c7f0 100644 --- a/packages/app/src/device/constants/response-status.constant.ts +++ b/packages/app/src/device/constants/response-status.constant.ts @@ -14,6 +14,7 @@ export enum ResponseStatus { NotAcceptable = 406, Conflict = 409, InternalServerError = 500, + NotImplemented = 501, UnprocessableEntity = 422, NotAccepted = 423, InsufficientStorage = 507, diff --git a/packages/app/src/device/controllers/device.controller.ts b/packages/app/src/device/controllers/device.controller.ts index 12fb477dbe..84ac41d1fb 100644 --- a/packages/app/src/device/controllers/device.controller.ts +++ b/packages/app/src/device/controllers/device.controller.ts @@ -19,11 +19,6 @@ export class DeviceController { return this.deviceService.connect() } - @IpcEvent(IpcDeviceEvent.Disconnect) - public async disconnectDevice(): Promise> { - return this.deviceService.disconnect() - } - @IpcEvent(IpcDeviceEvent.Unlock) public async unlockDevice(code: string): Promise> { return this.deviceService.unlock(code) diff --git a/packages/app/src/device/descriptors/device-descriptor.ts b/packages/app/src/device/descriptors/device-descriptor.ts index 3df6f7b1e4..312fc08cea 100644 --- a/packages/app/src/device/descriptors/device-descriptor.ts +++ b/packages/app/src/device/descriptors/device-descriptor.ts @@ -9,14 +9,24 @@ import { ProductID, DeviceType, } from "App/device/constants" -import { PureStrategy, HarmonyStrategy } from "App/device/strategies" +import { + PureStrategy, + HarmonyStrategy, + KompaktStrategy, +} from "App/device/strategies" import { SerialPortDeviceAdapter } from "App/device/modules/mudita-os/adapters/serial-port-device.adapters" +import { SerialPortDeviceKompaktAdapter } from "App/device/modules/mudita-os/adapters/serial-port-device-kompakt.adapters" export interface DeviceDescriptor { manufacturer: Manufacture deviceType: DeviceType productIds: ProductID[] vendorIds: VendorID[] - adapter: typeof SerialPortDeviceAdapter - strategy: typeof PureStrategy | typeof HarmonyStrategy + adapter: + | typeof SerialPortDeviceAdapter + | typeof SerialPortDeviceKompaktAdapter + strategy: + | typeof PureStrategy + | typeof HarmonyStrategy + | typeof KompaktStrategy } diff --git a/packages/app/src/device/descriptors/index.ts b/packages/app/src/device/descriptors/index.ts index 55da362f50..0739a5d30c 100644 --- a/packages/app/src/device/descriptors/index.ts +++ b/packages/app/src/device/descriptors/index.ts @@ -5,4 +5,5 @@ export * from "./mudita-pure.descriptor" export * from "./mudita-harmony.descriptor" +export * from "./mudita-kompakt.descriptor" export * from "./device-descriptor" diff --git a/packages/app/src/device/descriptors/mudita-kompakt.descriptor.ts b/packages/app/src/device/descriptors/mudita-kompakt.descriptor.ts new file mode 100644 index 0000000000..12092040f0 --- /dev/null +++ b/packages/app/src/device/descriptors/mudita-kompakt.descriptor.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + Manufacture, + VendorID, + ProductID, + DeviceType, +} from "App/device/constants" +import { KompaktStrategy } from "App/device/strategies" +import { SerialPortDeviceKompaktAdapter } from "App/device/modules/mudita-os/adapters" + +export class MuditaKompaktDescriptor { + static manufacturer = Manufacture.Mudita + static deviceType = DeviceType.MuditaKompakt + static productIds = [ + ProductID.MuditaKompaktChargeDec, + ProductID.MuditaKompaktTransferDec, + ProductID.MuditaKompaktNoDebugDec, + ProductID.MuditaKompaktChargeHex, + ProductID.MuditaKompaktTransferHex, + ProductID.MuditaKompaktNoDebugHex, + ] + static vendorIds = [VendorID.MuditaKompaktDec, VendorID.MuditaKompaktHex] + static adapter = SerialPortDeviceKompaktAdapter + static strategy = KompaktStrategy +} diff --git a/packages/app/src/device/descriptors/mudita-pure.descriptor.ts b/packages/app/src/device/descriptors/mudita-pure.descriptor.ts index c2cc52f91c..c44329192d 100644 --- a/packages/app/src/device/descriptors/mudita-pure.descriptor.ts +++ b/packages/app/src/device/descriptors/mudita-pure.descriptor.ts @@ -15,12 +15,8 @@ import { SerialPortDeviceAdapter } from "App/device/modules/mudita-os/adapters" export class MuditaPureDescriptor { static manufacturer = Manufacture.Mudita static deviceType = DeviceType.MuditaPure - static productIds = [ - ProductID.MuditaPure, - ProductID.MuditaPureTemporary, - ProductID.MuditaPureNotMtpTemporary, - ] - static vendorIds = [VendorID.MuditaPure, VendorID.MuditaPureTemporary] + static productIds = [ProductID.MuditaPure] + static vendorIds = [VendorID.MuditaPure] static adapter = SerialPortDeviceAdapter static strategy = PureStrategy } diff --git a/packages/app/src/device/listeners/register-device-unlocked.listener.ts b/packages/app/src/device/listeners/register-device-unlocked.listener.ts index 9a97264e1c..826b8a1a7b 100644 --- a/packages/app/src/device/listeners/register-device-unlocked.listener.ts +++ b/packages/app/src/device/listeners/register-device-unlocked.listener.ts @@ -7,9 +7,14 @@ import { ipcRenderer } from "electron-better-ipc" import store from "App/__deprecated__/renderer/store" import { unlockedDevice } from "App/device" import { DeviceIpcEvent } from "App/device/constants/device-ipc-event.constant" +import { Device } from "App/device/modules/device" +import { IpcRendererEvent } from "electron" -const deviceUnlockedHandler = (): void => { - void store.dispatch(unlockedDevice()) +const deviceUnlockedHandler = ( + _: IpcRendererEvent, + { agreementAccepted }: Device +): void => { + void store.dispatch(unlockedDevice(agreementAccepted)) } export const registerDeviceUnlockedListener = (): (() => void) => { diff --git a/packages/app/src/device/listeners/register-external-usage-device.listner.ts b/packages/app/src/device/listeners/register-external-usage-device.listner.ts new file mode 100644 index 0000000000..b5fa0a859a --- /dev/null +++ b/packages/app/src/device/listeners/register-external-usage-device.listner.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createClient } from "App/__deprecated__/api/mudita-center-server/create-client" +import { ipcMain } from "electron-better-ipc" +import { ExternalDataApiEvent } from "App/device/constants/external-data-api-event.constant" + +const registerExternalUsageDevice = (): void => { + const client = createClient() + + ipcMain.answerRenderer( + ExternalDataApiEvent.GetExternalUsageDevice, + (serialNumber: string) => { + return client.getExternalUsageDevice(serialNumber) + } + ) +} + +export default registerExternalUsageDevice diff --git a/packages/app/src/device/modules/base.adapter.ts b/packages/app/src/device/modules/base.adapter.ts index 183577685d..092542ef2d 100644 --- a/packages/app/src/device/modules/base.adapter.ts +++ b/packages/app/src/device/modules/base.adapter.ts @@ -3,14 +3,61 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import { ResultObject } from "App/core/builder" -import { RequestConfig, Response } from "App/device/types/mudita-os" +import SerialPort, { PortInfo } from "serialport" +import { EventEmitter } from "events" +import PQueue from "p-queue" +import { log, LogConfig } from "App/core/decorators/log.decorator" +import { Result, ResultObject } from "App/core/builder" +import { AppError } from "App/core/errors" import { DeviceCommunicationEvent } from "App/device/constants" +import { DeviceError } from "App/device/modules/mudita-os/constants" +import { + RequestConfig, + Response, + RequestPayload, +} from "App/device/types/mudita-os" export abstract class BaseAdapter { - constructor(public path: string) {} + protected serialPort: SerialPort + protected eventEmitter = new EventEmitter() + + protected requestsQueue = new PQueue({ concurrency: 1, interval: 1 }) + + constructor(public path: string) { + this.serialPort = new SerialPort(path, (error) => { + if (error) { + const appError = new AppError(DeviceError.Initialization, error.message) + this.emitInitializationFailedEvent(Result.failed(appError)) + + // workaround to trigger a device (USB) restart side effect after an initialization error + void this.getSerialPortList() + } else { + this.emitConnectionEvent(Result.success(`Device ${path} connected`)) + } + }) + } + + @log("==== serial port: disconnect ====") + public disconnect(): Promise> { + return new Promise((resolve) => { + if (this.serialPort === undefined) { + resolve(Result.success(true)) + } else { + this.serialPort.close((error) => { + if (error) { + resolve( + Result.failed( + new AppError(DeviceError.Disconnection, error.message) + ) + ) + } else { + resolve(Result.success(true)) + } + }) + } + }) + } - public abstract disconnect(): Promise> public abstract request( // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -18,12 +65,66 @@ export abstract class BaseAdapter { ): // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any Promise>> - public abstract on( - eventName: DeviceCommunicationEvent, - listener: () => void - ): void - public abstract off( - eventName: DeviceCommunicationEvent, - listener: () => void + + @log("==== serial port: connect event ====", LogConfig.Args) + protected emitConnectionEvent(data: ResultObject): void { + this.eventEmitter.emit(DeviceCommunicationEvent.Connected, data) + } + + @log("==== serial port: connection failed event ====", LogConfig.Args) + protected emitInitializationFailedEvent(data: ResultObject): void { + this.eventEmitter.emit(DeviceCommunicationEvent.InitializationFailed, data) + } + + @log("==== serial port: data received ====", LogConfig.Args) + protected emitDataReceivedEvent( + data: Response | AppError + ): void { + this.eventEmitter.emit(DeviceCommunicationEvent.DataReceived, data) + } + + @log("==== serial port: connection closed ====", LogConfig.Args) + protected emitCloseEvent(data: ResultObject): void { + this.eventEmitter.emit(DeviceCommunicationEvent.Disconnected, data) + } + + @log("==== serial port: list ====") + protected getSerialPortList(): Promise { + return SerialPort.list() + } + + public on(eventName: DeviceCommunicationEvent, listener: () => void): void { + this.eventEmitter.on(eventName, listener) + } + + public off(eventName: DeviceCommunicationEvent, listener: () => void): void { + this.eventEmitter.off(eventName, listener) + } + + protected abstract writeRequest( + port: SerialPort, + config: RequestConfig + ): // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Promise>> + + protected abstract deviceRequest( + port: SerialPort, + { options = {}, ...payload }: RequestPayload + ): // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Promise>> + + protected getNewUUID(): number { + return Math.floor(Math.random() * 10000) + } + + protected abstract mapPayloadToRequest(payload: unknown): string + + protected abstract portWrite( + port: SerialPort, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload: RequestPayload ): void } diff --git a/packages/app/src/device/modules/device.ts b/packages/app/src/device/modules/device.ts index 0fec6335f4..90636c5e5c 100644 --- a/packages/app/src/device/modules/device.ts +++ b/packages/app/src/device/modules/device.ts @@ -73,23 +73,6 @@ export class Device { } } - public async disconnect(): Promise> { - const response = await this.strategy.disconnect() - - this.unmountDeviceListeners() - - if (response) { - return Result.success(true) - } else { - return Result.failed( - new AppError( - DeviceCommunicationError.DisconnectionFailed, - `Cannot disconnect device ${this.path}` - ) - ) - } - } - public async request( config: RequestConfig ): Promise> { @@ -158,12 +141,18 @@ export class Device { } private mountListeners(): void { + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/unbound-method this.on(DeviceServiceEvent.DeviceConnected, this.emitConnectionEvent) this.on(DeviceServiceEvent.DeviceDisconnected, this.emitDisconnectionEvent) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/unbound-method this.on( DeviceServiceEvent.DeviceInitializationFailed, this.emitDeviceInitializationFailedEvent ) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/unbound-method this.on(DeviceServiceEvent.DeviceLocked, this.emitLockedEvent) this.on(DeviceServiceEvent.DeviceUnlocked, this.emitUnlockedEvent) this.on( @@ -179,10 +168,14 @@ export class Device { private unmountDeviceListeners(): void { this.off(DeviceServiceEvent.DeviceConnected, this.emitConnectionEvent) this.off(DeviceServiceEvent.DeviceDisconnected, this.emitDisconnectionEvent) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/unbound-method this.off( DeviceServiceEvent.DeviceInitializationFailed, this.emitDeviceInitializationFailedEvent ) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/unbound-method this.off(DeviceServiceEvent.DeviceLocked, this.emitLockedEvent) this.off(DeviceServiceEvent.DeviceUnlocked, this.emitUnlockedEvent) this.off( @@ -206,11 +199,9 @@ export class Device { } private emitDisconnectionEvent = (): void => { - if (!this.connecting) { - this.eventEmitter.emit(DeviceServiceEvent.DeviceDisconnected, this.path) - this.ipc.sendToRenderers(DeviceIpcEvent.DeviceDisconnected, this.path) - this.unmountDeviceListeners() - } + this.eventEmitter.emit(DeviceServiceEvent.DeviceDisconnected, this.path) + this.ipc.sendToRenderers(DeviceIpcEvent.DeviceDisconnected, this.path) + this.unmountDeviceListeners() } private emitDeviceInitializationFailedEvent = (): void => { diff --git a/packages/app/src/device/modules/mudita-os/adapters/index.ts b/packages/app/src/device/modules/mudita-os/adapters/index.ts index 48dc494407..97d463180b 100644 --- a/packages/app/src/device/modules/mudita-os/adapters/index.ts +++ b/packages/app/src/device/modules/mudita-os/adapters/index.ts @@ -4,3 +4,4 @@ */ export * from "./serial-port-device.adapters" +export * from "./serial-port-device-kompakt.adapters" diff --git a/packages/app/src/device/modules/mudita-os/adapters/serial-port-device-kompakt.adapters.ts b/packages/app/src/device/modules/mudita-os/adapters/serial-port-device-kompakt.adapters.ts new file mode 100644 index 0000000000..aafb9e67e5 --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/adapters/serial-port-device-kompakt.adapters.ts @@ -0,0 +1,141 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import SerialPort from "serialport" +import { log, LogConfig } from "App/core/decorators/log.decorator" +import { Result, ResultObject } from "App/core/builder" +import { AppError } from "App/core/errors" +import { CONNECTION_TIME_OUT_MS } from "App/device/constants" +import { DeviceCommunicationEvent, ResponseStatus } from "App/device/constants" +import { DeviceError } from "App/device/modules/mudita-os/constants" +import { SerialPortParserKompakt } from "App/device/modules/mudita-os/parsers" +import { + RequestConfig, + Response, + RequestPayload, +} from "App/device/types/mudita-os" +import { timeout } from "App/device/modules/mudita-os/helpers" +import { BaseAdapter } from "App/device/modules/base.adapter" +import { BodyKompakt } from "App/device/types/kompakt/body-kompakt.type" + +export class SerialPortDeviceKompaktAdapter extends BaseAdapter { + protected parser = new SerialPortParserKompakt() + + constructor(public path: string) { + super(path) + + this.serialPort.on("data", (buffer: Buffer) => { + try { + const data = this.parser.parse(buffer) + + if (data !== undefined) { + this.emitDataReceivedEvent(data) + } + } catch (error) { + this.emitDataReceivedEvent( + new AppError( + DeviceError.DataReceiving, + (error as Error).message || "Data receiving failed" + ) + ) + } + }) + + this.serialPort.on("close", () => { + this.emitCloseEvent(Result.success(`Device ${path} disconnected`)) + }) + } + + public async request( + config: RequestConfig + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): Promise>> { + if (this.serialPort === undefined) { + return Result.failed( + new AppError( + DeviceError.ConnectionDoesntEstablished, + "Serial port is undefined" + ), + { status: ResponseStatus.ConnectionError } + ) + } else { + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + const result = this.writeRequest(this.serialPort, config) + return result + } + } + + protected writeRequest( + port: SerialPort, + config: RequestConfig + ): Promise>> { + return new Promise>>((resolve) => { + const uuid = this.getNewUUID() + const payload: RequestPayload = { ...config, uuid } + + void this.requestsQueue.add(async () => { + const response = await this.deviceRequest(port, payload) + resolve(response) + }) + }) + } + + protected deviceRequest( + port: SerialPort, + { options = {}, ...payload }: RequestPayload + ): Promise>> { + const connectionTimeOut = + options?.connectionTimeOut ?? CONNECTION_TIME_OUT_MS + return new Promise((resolve) => { + const [promise, cancel] = timeout(connectionTimeOut) + void promise.then(() => { + resolve( + Result.failed( + new AppError( + DeviceError.TimeOut, + `Cannot receive response from ${this.path}` + ), + { + status: ResponseStatus.Timeout, + ...payload, + } + ) + ) + }) + + const listener = (response: Response) => { + if (response.uuid === payload.uuid) { + this.eventEmitter.off(DeviceCommunicationEvent.DataReceived, listener) + cancel() + const result = response.error + ? Result.failed( + new AppError(DeviceError.RequestFailed, response.error.message) + ) + : Result.success(response) + resolve(result) + } + } + + this.eventEmitter.on(DeviceCommunicationEvent.DataReceived, listener) + this.portWrite(port, payload) + }) + } + + @log("==== serial port: create valid request ====", LogConfig.Args) + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected mapPayloadToRequest(payload: RequestPayload): string { + return this.parser.createRequest(payload) + } + + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected portWrite(port: SerialPort, payload: RequestPayload): void { + const request = this.mapPayloadToRequest(payload) + port.write(request) + } +} diff --git a/packages/app/src/device/modules/mudita-os/adapters/serial-port-device.adapters.ts b/packages/app/src/device/modules/mudita-os/adapters/serial-port-device.adapters.ts index 787ac1b9fe..6ba8224b9b 100644 --- a/packages/app/src/device/modules/mudita-os/adapters/serial-port-device.adapters.ts +++ b/packages/app/src/device/modules/mudita-os/adapters/serial-port-device.adapters.ts @@ -3,9 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import SerialPort, { PortInfo } from "serialport" -import { EventEmitter } from "events" -import PQueue from "p-queue" +import SerialPort from "serialport" import { log, LogConfig } from "App/core/decorators/log.decorator" import { Result, ResultObject } from "App/core/builder" import { AppError } from "App/core/errors" @@ -22,26 +20,11 @@ import { timeout } from "App/device/modules/mudita-os/helpers" import { BaseAdapter } from "App/device/modules/base.adapter" export class SerialPortDeviceAdapter extends BaseAdapter { - private serialPort: SerialPort - private eventEmitter = new EventEmitter() - private parser = new SerialPortParser() - private requestsQueue = new PQueue({ concurrency: 1, interval: 1 }) + protected parser = new SerialPortParser() constructor(public path: string) { super(path) - this.serialPort = new SerialPort(path, (error) => { - if (error) { - const appError = new AppError(DeviceError.Initialization, error.message) - this.emitInitializationFailedEvent(Result.failed(appError)) - - // workaround to trigger a device (USB) restart side effect after an initialization error - void this.getSerialPortList() - } else { - this.emitConnectionEvent(Result.success(`Device ${path} connected`)) - } - }) - this.serialPort.on("data", (event) => { try { const data = this.parser.parse(event) @@ -64,32 +47,6 @@ export class SerialPortDeviceAdapter extends BaseAdapter { }) } - @log("==== serial port: disconnect ====") - public disconnect(): Promise> { - return new Promise((resolve) => { - if (this.serialPort === undefined) { - resolve(Result.success(true)) - } else { - this.serialPort.close((error) => { - if (error) { - resolve( - Result.failed( - new AppError(DeviceError.Disconnection, error.message) - ) - ) - } else { - resolve(Result.success(true)) - } - }) - } - }) - } - - public async request( - config: RequestConfig - ): // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Promise>> public async request( config: RequestConfig // AUTO DISABLED - fix me if you like :) @@ -104,21 +61,11 @@ export class SerialPortDeviceAdapter extends BaseAdapter { { status: ResponseStatus.ConnectionError } ) } else { - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return this.writeRequest(this.serialPort, config) } } - public on(eventName: DeviceCommunicationEvent, listener: () => void): void { - this.eventEmitter.on(eventName, listener) - } - - public off(eventName: DeviceCommunicationEvent, listener: () => void): void { - this.eventEmitter.off(eventName, listener) - } - - private writeRequest( + protected writeRequest( port: SerialPort, config: RequestConfig // AUTO DISABLED - fix me if you like :) @@ -131,12 +78,13 @@ export class SerialPortDeviceAdapter extends BaseAdapter { const payload: RequestPayload = { ...config, uuid } void this.requestsQueue.add(async () => { - resolve(await this.deviceRequest(port, payload)) + const response = await this.deviceRequest(port, payload) + resolve(response) }) }) } - private deviceRequest( + protected deviceRequest( port: SerialPort, { options = {}, ...payload }: RequestPayload ): // AUTO DISABLED - fix me if you like :) @@ -184,45 +132,17 @@ export class SerialPortDeviceAdapter extends BaseAdapter { }) } - private getNewUUID(): number { - return Math.floor(Math.random() * 10000) - } - + @log("==== serial port: create valid request ====", LogConfig.Args) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any - private portWrite(port: SerialPort, payload: RequestPayload): void { - port.write(this.mapPayloadToRequest(payload)) - } - - @log("==== serial port: connect event ====", LogConfig.Args) - private emitConnectionEvent(data: ResultObject): void { - this.eventEmitter.emit(DeviceCommunicationEvent.Connected, data) - } - - @log("==== serial port: connection failed event ====", LogConfig.Args) - private emitInitializationFailedEvent(data: ResultObject): void { - this.eventEmitter.emit(DeviceCommunicationEvent.InitializationFailed, data) + protected mapPayloadToRequest(payload: RequestPayload): string { + return this.parser.createRequest(payload) } - @log("==== serial port: data received ====", LogConfig.Args) - private emitDataReceivedEvent(data: Response | AppError): void { - this.eventEmitter.emit(DeviceCommunicationEvent.DataReceived, data) - } - - @log("==== serial port: connection closed ====", LogConfig.Args) - private emitCloseEvent(data: ResultObject): void { - this.eventEmitter.emit(DeviceCommunicationEvent.Disconnected, data) - } - - @log("==== serial port: create valid request ====", LogConfig.Args) // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any - private mapPayloadToRequest(payload: RequestPayload): string { - return SerialPortParser.createValidRequest(payload) - } - - @log("==== serial port: list ====") - private getSerialPortList(): Promise { - return SerialPort.list() + protected portWrite(port: SerialPort, payload: RequestPayload): void { + const request = this.mapPayloadToRequest(payload) + port.write(request) } } diff --git a/packages/app/src/device/modules/mudita-os/parsers/index.ts b/packages/app/src/device/modules/mudita-os/parsers/index.ts index 034fb8ffab..7d32c90369 100644 --- a/packages/app/src/device/modules/mudita-os/parsers/index.ts +++ b/packages/app/src/device/modules/mudita-os/parsers/index.ts @@ -4,3 +4,4 @@ */ export * from "./serial-port.parser" +export * from "./serial-port-kompakt.parser" diff --git a/packages/app/src/device/modules/mudita-os/parsers/serial-port-base.parser.ts b/packages/app/src/device/modules/mudita-os/parsers/serial-port-base.parser.ts new file mode 100644 index 0000000000..09c0af0803 --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/parsers/serial-port-base.parser.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { RequestPayload, Response } from "App/device/types/mudita-os" + +export abstract class SerialPortParserBase { + public abstract parse(data: Buffer): Response | undefined + + public abstract createRequest(payload: RequestPayload): string +} diff --git a/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.test.ts b/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.test.ts new file mode 100644 index 0000000000..9ca1e14f39 --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.test.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { SerialPortParserKompakt } from "App/device/modules/mudita-os/parsers" +import { RequestPayload } from "App/device/types/mudita-os" +import { PrefixKompakt } from "App/device/constants/prefix-kompakt.constant" +import { HeaderKompakt } from "App/device/types/kompakt/header-kompakt.type" +import { PayloadKompakt } from "App/device/types/kompakt/payload-kompakt.type" + +describe("`Parser.createRequest`", () => { + const parser = new SerialPortParserKompakt() + test("request with empty body", () => { + const payload: RequestPayload = { + endpoint: 1, + method: 1, + uuid: 0, + } + + const input = parser.createRequest(payload) + const output = `${PrefixKompakt.REQUEST}000000055000000002{"endpoint":1,"method":1,"offset":0,"limit":1,"uuid":0}{}` + + expect(input).toEqual(output) + }) + + test("request with body", () => { + type Body = { + whatever: string + someNum: number + } + + const payload: RequestPayload = { + endpoint: 1, + method: 1, + uuid: 0, + body: { + whatever: "blabla", + someNum: 123, + }, + } + + const input = parser.createRequest(payload) + const output = `${PrefixKompakt.REQUEST}000000055000000035{"endpoint":1,"method":1,"offset":0,"limit":1,"uuid":0}{"whatever":"blabla","someNum":123}` + + expect(input).toEqual(output) + }) +}) + +const generateBuffor = ( + headerObj: HeaderKompakt, + payloadObj: PayloadKompakt +): Buffer => { + const prefixBuff = Buffer.from(PrefixKompakt.RESPONSE, "utf-8") + + const payload = JSON.stringify(payloadObj) + const payloadBase64 = new Buffer(payload).toString("base64") + const payloadBase64Buff = Buffer.from(payloadBase64, "utf-8") + + const header = JSON.stringify(headerObj) + const headerBuff = Buffer.from(header, "utf-8") + + const headerSize = String(header.length).padStart(9, "0") + const headerSizeBuff = Buffer.from(headerSize, "utf-8") + + const payloadSize = String(payloadBase64.length).padStart(9, "0") + const payloadSizeBuff = Buffer.from(payloadSize, "utf-8") + + return Buffer.concat([ + prefixBuff, + headerSizeBuff, + payloadSizeBuff, + headerBuff, + payloadBase64Buff, + ]) +} + +describe("SerialPortParserKompakt.parse", () => { + const parser = new SerialPortParserKompakt() + + test("regular case", () => { + const payloadObj = { + serialNumber: "12345678901234", + version: 31, + batteryLevel: "68", + batteryState: 2, + batteryCapacity: 76, + simCards: [ + { + simSlot: 0, + networkOperatorName: "Play", + signalStrength: 2, + networkStatus: 1, + }, + ], + } + + const headerObj = { + endpoint: 1, + method: 1, + offset: 0, + limit: 1, + uuid: 5092, + status: 200, + contd: true, + } + + const responseBuff = generateBuffor(headerObj, payloadObj) + + const { status, uuid, ...restHeader } = headerObj + const result = { + body: { + ...payloadObj, + ...restHeader, + }, + error: undefined, + status, + uuid, + } + expect(parser.parse(responseBuff)).toEqual(result) + }) + + test("empty simCards - no card inserted", () => { + const payloadObj = { + serialNumber: "12345678901234", + version: 31, + batteryLevel: "68", + batteryState: 2, + batteryCapacity: 76, + simCards: [], + } + + const headerObj = { + endpoint: 1, + method: 1, + offset: 0, + limit: 1, + uuid: 5092, + status: 200, + contd: true, + } + + const responseBuff = generateBuffor(headerObj, payloadObj) + + const { status, uuid, ...restHeader } = headerObj + const result = { + body: { + ...payloadObj, + ...restHeader, + }, + error: undefined, + status, + uuid, + } + expect(parser.parse(responseBuff)).toEqual(result) + }) +}) diff --git a/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.ts b/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.ts new file mode 100644 index 0000000000..8e5ce93020 --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/parsers/serial-port-kompakt.parser.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { TextEncoder } from "util" +import { RequestPayload, Response } from "App/device/types/mudita-os" +import { SerialPortParserBase } from "App/device/modules/mudita-os/parsers/serial-port-base.parser" +import { HeaderKompakt } from "App/device/types/kompakt/header-kompakt.type" +import { PayloadKompakt } from "App/device/types/kompakt/payload-kompakt.type" +import { BodyKompakt } from "App/device/types/kompakt/body-kompakt.type" +import { PrefixKompakt } from "App/device/constants/prefix-kompakt.constant" + +export class SerialPortParserKompakt extends SerialPortParserBase { + public parse(data: Buffer): Response { + try { + const prefix = String.fromCharCode(data[0]) + + if (prefix === PrefixKompakt.RESPONSE) { + const headerSize = Number(data.subarray(1, 10).valueOf()) + const payloadSize = Number(data.subarray(10, 19).valueOf()) + + const headerBuffer = data.subarray(19, 19 + headerSize) + const payloadBuffer = data.subarray( + 19 + headerSize, + 19 + headerSize + payloadSize + ) + + const header = JSON.parse(headerBuffer.toString()) as HeaderKompakt + const payload = this.parsePayload(payloadBuffer) + + const result = this.mapToResponse(header, payload) + return result + } else { + throw new Error("Invalid or unknown data type") + } + } catch (ex) { + throw new Error("Could not parse the response - ") + } + } + + private parsePayload(buffer: Buffer): PayloadKompakt { + const payloadrSingleLine = buffer.toString().split("\n").join() + const payloadDecoded = Buffer.from(payloadrSingleLine, "base64").toString() + return JSON.parse(payloadDecoded) as PayloadKompakt + } + + private mapToResponse( + { status, uuid, ...headerRest }: HeaderKompakt, + { message, ...kompaktRest }: PayloadKompakt + ): Response { + return { + status, + body: { + ...kompaktRest, + ...headerRest, + }, + error: message !== undefined ? { message } : undefined, + uuid, + } as Response + } + + public createRequest(payload: RequestPayload): string { + const encoder = new TextEncoder() + + const { + endpoint, + method, + offset = 0, + limit = 1, + uuid, + ...payloadRest + } = payload + + const headerAsString = JSON.stringify({ + endpoint, + method, + offset, + limit, + uuid, + }) + const headerLength = String(encoder.encode(headerAsString).length).padStart( + 9, + "0" + ) + + const { body, ...rest } = payloadRest + const bodyAsString = JSON.stringify({ ...body, ...rest }) + const bodyLength = String(encoder.encode(bodyAsString).length).padStart( + 9, + "0" + ) + + return `${PrefixKompakt.REQUEST}${headerLength}${bodyLength}${headerAsString}${bodyAsString}` + } +} diff --git a/packages/app/src/device/modules/mudita-os/parsers/serial-port-parser.test.ts b/packages/app/src/device/modules/mudita-os/parsers/serial-port-parser.test.ts new file mode 100644 index 0000000000..5ead2ad222 --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/parsers/serial-port-parser.test.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { SerialPortParser } from "./serial-port.parser" +import { RequestPayload } from "App/device/types/mudita-os" + +describe("`Parser.createValidRequest`", () => { + const parser = new SerialPortParser() + test("`payload` created properly ", () => { + const payload: RequestPayload = { + endpoint: 1, + method: 1, + uuid: 0, + } + expect(parser.createRequest(payload)).toEqual( + '#000000034{"endpoint":1,"method":1,"uuid":0}' + ) + }) +}) + +describe("`Parser.parse`", () => { + describe("when data is packed in single packet", () => { + const parser = new SerialPortParser() + const endpoint = Buffer.from([35]) + const paylaod = { entry: [] } + const payloadStringify = JSON.stringify(paylaod) + const payloadStringifyLength = payloadStringify.length + const size = Buffer.from( + String(payloadStringifyLength).padStart(9, "0"), + "utf-8" + ) + const payload = Buffer.from(payloadStringify, "utf-8") + const buffer = Buffer.concat([endpoint, size, payload]) + + test("payload is return properly", () => { + expect(parser.parse(buffer)).toEqual(paylaod) + }) + }) + + describe("when endpoint is unknown", () => { + const parser = new SerialPortParser() + const endpoint = Buffer.from([999]) + const paylaod = { entry: [] } + const payloadStringify = JSON.stringify(paylaod) + const payloadStringifyLength = payloadStringify.length + const size = Buffer.from( + String(payloadStringifyLength).padStart(9, "0"), + "utf-8" + ) + const payload = Buffer.from(payloadStringify, "utf-8") + const buffer = Buffer.concat([endpoint, size, payload]) + + test("method thrown error", () => { + expect(() => parser.parse(buffer)).toThrow() + }) + }) + + describe("when size is NaN", () => { + const parser = new SerialPortParser() + const endpoint = Buffer.from([35]) + const paylaod = { entry: [] } + const payloadStringify = JSON.stringify(paylaod) + const size = Buffer.from(String("bad_number").padStart(9, "0"), "utf-8") + const payload = Buffer.from(payloadStringify, "utf-8") + const buffer = Buffer.concat([endpoint, size, payload]) + + test("method thrown error", () => { + expect(() => parser.parse(buffer)).toThrow() + }) + }) + + describe("when data is chunks to more than one packet", () => { + const parser = new SerialPortParser() + const endpoint = Buffer.from([35]) + const paylaod = { entry: [] } + const payloadStringify = JSON.stringify(paylaod) + const payloadStringifyLength = JSON.stringify(paylaod).length + const firstPayload = payloadStringify.slice(0, 6) + const secondPayload = payloadStringify.slice(6) + const size = Buffer.from( + String(payloadStringifyLength).padStart(9, "0"), + "utf-8" + ) + const firstPacketPayload = Buffer.from(firstPayload, "utf-8") + const firstBuffer = Buffer.concat([endpoint, size, firstPacketPayload]) + const secondPacketPayload = Buffer.from(secondPayload, "utf-8") + const secondBuffer = Buffer.concat([secondPacketPayload]) + + test("method concatenate whole payload", () => { + parser.parse(firstBuffer) + expect(parser.parse(secondBuffer)).toEqual(paylaod) + }) + }) +}) diff --git a/packages/app/src/device/modules/mudita-os/parsers/serial-port.parser.ts b/packages/app/src/device/modules/mudita-os/parsers/serial-port.parser.ts index feab32e03f..648dd0b69c 100644 --- a/packages/app/src/device/modules/mudita-os/parsers/serial-port.parser.ts +++ b/packages/app/src/device/modules/mudita-os/parsers/serial-port.parser.ts @@ -6,8 +6,9 @@ import { TextEncoder } from "util" import { RequestPayload, Response } from "App/device/types/mudita-os" import { PacketType } from "App/device/modules/mudita-os/constants" +import { SerialPortParserBase } from "App/device/modules/mudita-os/parsers/serial-port-base.parser" -export class SerialPortParser { +export class SerialPortParser extends SerialPortParserBase { private dataRaw = Buffer.alloc(0) private dataSizeToRead = -1 private needMoreData = false @@ -18,7 +19,6 @@ export class SerialPortParser { } const endpoint = data[0] - if ( !(endpoint === PacketType.Endpoint || endpoint === PacketType.RawData) ) { @@ -27,15 +27,14 @@ export class SerialPortParser { this.dataRaw = Buffer.alloc(0) - const size = Number(data.slice(1, 10)) + const size = Number(data.subarray(1, 10)) if (isNaN(size) || size === 0) { throw new Error(`Can't parse data size as number`) } this.dataSizeToRead = size - - return this.readPayload(data.slice(10)) + return this.readPayload(data.subarray(10)) } private readPayload(buffer: Buffer): undefined | Response { @@ -55,7 +54,7 @@ export class SerialPortParser { } } - static createValidRequest(payload: RequestPayload): string { + public createRequest(payload: RequestPayload): string { const encoder = new TextEncoder() let requestStr = "#" diff --git a/packages/app/src/device/modules/mudita-os/presenters/index.ts b/packages/app/src/device/modules/mudita-os/presenters/index.ts index 4c6d00d00f..9b88ce5ddb 100644 --- a/packages/app/src/device/modules/mudita-os/presenters/index.ts +++ b/packages/app/src/device/modules/mudita-os/presenters/index.ts @@ -4,3 +4,4 @@ */ export * from "./response.presenter" +export * from "./response-kompakt.presenter" diff --git a/packages/app/src/device/modules/mudita-os/presenters/response-kompakt.presenter.ts b/packages/app/src/device/modules/mudita-os/presenters/response-kompakt.presenter.ts new file mode 100644 index 0000000000..d0ae42609a --- /dev/null +++ b/packages/app/src/device/modules/mudita-os/presenters/response-kompakt.presenter.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ResultObject } from "App/core/builder" +import { Response } from "App/device/types/mudita-os" +import { RequestResponse, RequestResponseStatus } from "App/core/types" +import { BodyKompakt } from "App/device/types/kompakt/body-kompakt.type" +import { SIM, Tray } from "App/device/constants" + +export class ResponseKompaktPresenter { + static toResponseObject( + response: ResultObject> + ): RequestResponse { + const data = response.data as Response + const simCard = data.body?.simCards[0] + + return { + status: response.ok + ? RequestResponseStatus.Ok + : RequestResponseStatus.Error, + data: { + //from device + serialNumber: data.body?.serialNumber, + batteryLevel: data.body?.batteryCapacity, + batteryState: data.body?.batteryState, + version: data.body?.version, + signalStrength: simCard?.signalStrength, + networkOperatorName: simCard?.networkOperatorName, + networkStatus: simCard?.networkStatus, + + //mocked + accessTechnology: "255", + backupFilePath: "/user/temp/backup.tar", + caseColour: "black", + currentRTCTime: "1686307333", + deviceSpaceTotal: "14951", + deviceToken: "RIQLcvFFgl8ibFcHwBO3Ev0YTa2vxfbI", + gitBranch: "HEAD", + gitRevision: "4cd97006", + mtpPath: "/user/media/app/music_player", + selectedSim: SIM.None, + recoveryStatusFilePath: "/user/temp/recovery_status.json", + syncFilePath: "/user/temp/sync.tar", + systemReservedSpace: "2048", + trayState: Tray.Out, + updateFilePath: "/user/temp/update.tar", + usedUserSpace: "438", + }, + } + } +} diff --git a/packages/app/src/device/reducers/device.interface.ts b/packages/app/src/device/reducers/device.interface.ts index e9f61d1ab8..5de2634af0 100644 --- a/packages/app/src/device/reducers/device.interface.ts +++ b/packages/app/src/device/reducers/device.interface.ts @@ -12,6 +12,21 @@ import { ConnectionState } from "App/device/constants" import { AppError } from "App/core/errors" import StorageInfo from "App/__deprecated__/common/interfaces/storage-info.interface" +export interface KompaktDeviceData { + networkName: string + networkLevel: string + osVersion: string + batteryLevel: number + simCards: SimCard[] + serialNumber: string + memorySpace: { + reservedSpace: number + usedUserSpace: number + total: number + } + caseColour: CaseColor +} + export interface PureDeviceData { networkName: string networkLevel: string @@ -43,7 +58,7 @@ export interface HarmonyDeviceData { export interface DeviceState { deviceType: DeviceType | null - data: Partial | null + data: Partial | null state: ConnectionState status: { connecting: boolean @@ -52,8 +67,10 @@ export interface DeviceState { loaded: boolean agreementAccepted: boolean criticalBatteryLevel: boolean + restarting: boolean } error: Error | string | null + externalUsageDevice: boolean | null } export interface OsVersionPayload { diff --git a/packages/app/src/device/reducers/device.reducer.test.ts b/packages/app/src/device/reducers/device.reducer.test.ts index b572daae00..1e6bb02dc4 100644 --- a/packages/app/src/device/reducers/device.reducer.test.ts +++ b/packages/app/src/device/reducers/device.reducer.test.ts @@ -142,21 +142,6 @@ describe("Connecting/Disconnecting functionality", () => { ).toEqual(initialState) }) - test("Event: Disconnected/rejected set error message and updates state to error", () => { - const errorMock = new AppError(DeviceError.Connection, "I'm error") - - expect( - deviceReducer(undefined, { - type: rejectedAction(DeviceEvent.Disconnected), - payload: errorMock, - }) - ).toEqual({ - ...initialState, - state: ConnectionState.Error, - error: errorMock, - }) - }) - test("Event: SetConnectionState/fulfilled set connection state to `true` if `true` payload is provided", () => { expect( deviceReducer(undefined, { @@ -335,7 +320,6 @@ describe("Updates loading functionality", () => { state: ConnectionState.Loading, }) }) - test("Event: Loading/fulfilled change `state` to Loaded", () => { expect( deviceReducer(undefined, { @@ -488,6 +472,7 @@ describe("`LoadStorageInfo` functionality", () => { }, "deviceType": null, "error": null, + "externalUsageDevice": null, "state": 2, "status": Object { "agreementAccepted": true, @@ -495,6 +480,7 @@ describe("`LoadStorageInfo` functionality", () => { "connecting": false, "criticalBatteryLevel": false, "loaded": false, + "restarting": false, "unlocked": null, }, } diff --git a/packages/app/src/device/reducers/device.reducer.ts b/packages/app/src/device/reducers/device.reducer.ts index 739f760f40..b2b4071867 100644 --- a/packages/app/src/device/reducers/device.reducer.ts +++ b/packages/app/src/device/reducers/device.reducer.ts @@ -16,7 +16,6 @@ import { PureDeviceData, ConnectedFulfilledAction, ConnectedRejectedAction, - DisconnectedRejectedAction, SetDeviceDataAction, LoadDataRejectAction, SetPhoneLockTimeAction, @@ -29,6 +28,9 @@ import { import { setAgreementStatus, setCriticalBatteryLevel, + setExternalUsageDevice, + setRestarting, + unlockedDevice, } from "App/device/actions/base.action" export const initialState: DeviceState = { @@ -41,9 +43,11 @@ export const initialState: DeviceState = { loaded: false, agreementAccepted: true, criticalBatteryLevel: false, + restarting: false, }, state: ConnectionState.Empty, error: null, + externalUsageDevice: null, } export const deviceReducer = createReducer( @@ -69,6 +73,7 @@ export const deviceReducer = createReducer( loaded: false, }, error: null, + externalUsageDevice: null, } }) .addCase( @@ -106,6 +111,7 @@ export const deviceReducer = createReducer( ...state, state: ConnectionState.Loading, error: null, + externalUsageDevice: null, } }) .addCase(fulfilledAction(DeviceEvent.Disconnected), (state) => { @@ -115,21 +121,6 @@ export const deviceReducer = createReducer( error: null, } }) - .addCase( - rejectedAction(DeviceEvent.Disconnected), - (state, action: DisconnectedRejectedAction) => { - return { - ...state, - status: { - ...state.status, - connected: false, - connecting: false, - }, - state: ConnectionState.Error, - error: action.payload, - } - } - ) .addCase( fulfilledAction(DeviceEvent.SetConnectionState), (state, action: SetConnectionStateAction) => { @@ -156,12 +147,15 @@ export const deviceReducer = createReducer( }, } }) - .addCase(DeviceEvent.Unlocked, (state) => { + .addCase(unlockedDevice, (state, { payload: agreementAccepted }) => { return { ...state, status: { ...state.status, unlocked: true, + agreementAccepted: agreementAccepted + ? agreementAccepted + : state.status.agreementAccepted, }, error: null, } @@ -237,7 +231,6 @@ export const deviceReducer = createReducer( } } ) - // Updates loading data state .addCase(pendingAction(DeviceEvent.Loading), (state) => { return { @@ -302,5 +295,11 @@ export const deviceReducer = createReducer( .addCase(setCriticalBatteryLevel, (state, action) => { state.status.criticalBatteryLevel = action.payload }) + .addCase(setExternalUsageDevice, (state, action) => { + state.externalUsageDevice = action.payload + }) + .addCase(setRestarting, (state, action) => { + state.status.restarting = action.payload + }) } ) diff --git a/packages/app/src/device/requests/external-usage-device.request.ts b/packages/app/src/device/requests/external-usage-device.request.ts new file mode 100644 index 0000000000..cd9e831096 --- /dev/null +++ b/packages/app/src/device/requests/external-usage-device.request.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { ipcRenderer } from "electron-better-ipc" +import { ExternalDataApiEvent } from "App/device/constants/external-data-api-event.constant" + +export const externalUsageDevice = async ( + serialNumber: string +): Promise => { + return ipcRenderer.callMain( + ExternalDataApiEvent.GetExternalUsageDevice, + serialNumber + ) +} diff --git a/packages/app/src/device/requests/index.ts b/packages/app/src/device/requests/index.ts index 6a205607a5..dbf62aa1d3 100644 --- a/packages/app/src/device/requests/index.ts +++ b/packages/app/src/device/requests/index.ts @@ -5,6 +5,5 @@ export * from "./connect-device.request" export * from "./device-lock-time.request" -export * from "./disconnect-device.request" export * from "./unlock-device-status.request" export * from "./unlock-device.request" diff --git a/packages/app/src/device/services/device.service.ts b/packages/app/src/device/services/device.service.ts index ac1aa09387..6ceff3b9fd 100644 --- a/packages/app/src/device/services/device.service.ts +++ b/packages/app/src/device/services/device.service.ts @@ -24,10 +24,6 @@ export class DeviceService { return this.deviceManager.device.connect() } - public async disconnect(): Promise> { - return this.deviceManager.device.disconnect() - } - public async unlock(code: string): Promise> { const response = await this.deviceManager.device.request({ endpoint: Endpoint.Security, diff --git a/packages/app/src/device/strategies/device-strategy.class.ts b/packages/app/src/device/strategies/device-strategy.class.ts index 419010566a..9d7f55ab54 100644 --- a/packages/app/src/device/strategies/device-strategy.class.ts +++ b/packages/app/src/device/strategies/device-strategy.class.ts @@ -19,9 +19,6 @@ export interface DeviceStrategy { connect(): Promise> // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any - disconnect(): Promise - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-explicit-any request(config: RequestConfig): Promise on( eventName: DeviceServiceEvent, diff --git a/packages/app/src/device/strategies/harmony.strategy.ts b/packages/app/src/device/strategies/harmony.strategy.ts index 9bb348b936..a62bf8d309 100644 --- a/packages/app/src/device/strategies/harmony.strategy.ts +++ b/packages/app/src/device/strategies/harmony.strategy.ts @@ -42,19 +42,6 @@ export class HarmonyStrategy implements DeviceStrategy { return response } - public async disconnect(): Promise { - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - - const response = await this.adapter.disconnect() - - this.unmountDisconnectionListener() - this.unmountInitializationFailedListener() - this.eventEmitter.emit(DeviceServiceEvent.DeviceDisconnected) - - return Boolean(response.data) - } - public request( config: GetDeviceInfoRequestConfig ): Promise> diff --git a/packages/app/src/device/strategies/index.ts b/packages/app/src/device/strategies/index.ts index 7fc70234cb..0106216be5 100644 --- a/packages/app/src/device/strategies/index.ts +++ b/packages/app/src/device/strategies/index.ts @@ -5,3 +5,4 @@ export * from "./harmony.strategy" export * from "./pure.strategy" +export * from "./kompakt.strategy" diff --git a/packages/app/src/device/strategies/kompakt.strategy.ts b/packages/app/src/device/strategies/kompakt.strategy.ts new file mode 100644 index 0000000000..a7a8565b8c --- /dev/null +++ b/packages/app/src/device/strategies/kompakt.strategy.ts @@ -0,0 +1,123 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { EventEmitter } from "events" +import { RequestResponse, RequestResponseStatus } from "App/core/types" +import { DeviceStrategy } from "App/device/strategies/device-strategy.class" +import { DeviceInfo, RequestConfig } from "../types/mudita-os" +import { BaseAdapter } from "App/device/modules/base.adapter" +import { + GetDeviceInfoResponseBody, + GetDeviceInfoRequestConfig, +} from "App/device/types/mudita-os" +import { + Method, + Endpoint, + DeviceCommunicationEvent, + DeviceServiceEvent, +} from "App/device/constants" +import { ResponseKompaktPresenter } from "App/device/modules/mudita-os/presenters" + +export class KompaktStrategy implements DeviceStrategy { + private eventEmitter = new EventEmitter() + private lockedInterval: NodeJS.Timeout | undefined + + constructor(private adapter: BaseAdapter) { + EventEmitter.defaultMaxListeners = 15 + this.mountDisconnectionListener() + this.mountInitializationFailedListener() + } + + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async connect(): Promise> { + const response = await this.request({ + endpoint: Endpoint.DeviceInfo, + method: Method.Get, + }) + + if ( + response.status === RequestResponseStatus.Ok || + response.status === RequestResponseStatus.PhoneLocked + ) { + this.eventEmitter.emit(DeviceServiceEvent.DeviceConnected) + } + + return response + } + + public async request( + config: GetDeviceInfoRequestConfig + ): Promise> + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars + async request(config: RequestConfig): Promise { + let result = { + status: RequestResponseStatus.Ok, + data: undefined, + error: undefined, + } as RequestResponse + //CP-1668 - this condition until Kompakt has limited endpoint support, currently only device info endpoint (10.08.2023) + if ([Endpoint.DeviceInfo].includes(config.endpoint)) { + const response = await this.adapter.request(config) + result = ResponseKompaktPresenter.toResponseObject(response) + } + + this.emitUnlockEvent() + return result + } + on( + eventName: DeviceServiceEvent, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listener: (path: string, ...args: any[]) => void + ): void { + this.eventEmitter.on(eventName, listener) + } + off( + eventName: DeviceServiceEvent, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listener: (path: string, ...args: any[]) => void + ): void { + this.eventEmitter.off(eventName, listener) + } + onCommunicationEvent( + eventName: DeviceCommunicationEvent, + listener: () => void + ): void { + this.adapter.on(eventName, listener) + } + offCommunicationEvent( + eventName: DeviceCommunicationEvent, + listener: () => void + ): void { + this.adapter.off(eventName, listener) + } + + private unmountDeviceUnlockedListener(): void { + clearInterval(this.lockedInterval) + } + + private mountDisconnectionListener(): void { + this.onCommunicationEvent(DeviceCommunicationEvent.Disconnected, () => { + this.eventEmitter.emit(DeviceServiceEvent.DeviceDisconnected) + this.unmountDeviceUnlockedListener() + }) + } + + private mountInitializationFailedListener(): void { + this.onCommunicationEvent( + DeviceCommunicationEvent.InitializationFailed, + () => { + this.eventEmitter.emit(DeviceServiceEvent.DeviceInitializationFailed) + } + ) + } + + private emitUnlockEvent(): void { + this.eventEmitter.emit(DeviceServiceEvent.DeviceUnlocked) + } +} diff --git a/packages/app/src/device/strategies/pure.strategy.ts b/packages/app/src/device/strategies/pure.strategy.ts index a902c7c2f8..277773fbdd 100644 --- a/packages/app/src/device/strategies/pure.strategy.ts +++ b/packages/app/src/device/strategies/pure.strategy.ts @@ -113,17 +113,6 @@ export class PureStrategy implements DeviceStrategy { return response } - public async disconnect(): Promise { - const response = await this.adapter.disconnect() - - this.unmountDeviceUnlockedListener() - this.unmountDisconnectionListener() - this.unmountInitializationFailedListener() - this.eventEmitter.emit(DeviceServiceEvent.DeviceDisconnected) - - return Boolean(response.data) - } - public async request( config: GetSecurityRequestConfig ): Promise @@ -142,7 +131,6 @@ export class PureStrategy implements DeviceStrategy { public async request( config: GetDeviceFilesRequestConfig ): Promise> - public async request( config: GetMessagesRequestConfig ): Promise> diff --git a/packages/app/src/device/types/kompakt/body-kompakt.type.ts b/packages/app/src/device/types/kompakt/body-kompakt.type.ts new file mode 100644 index 0000000000..d45ee4a10d --- /dev/null +++ b/packages/app/src/device/types/kompakt/body-kompakt.type.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { PayloadKompakt } from "App/device/types/kompakt/payload-kompakt.type" +import { HeaderKompakt } from "App/device/types/kompakt/header-kompakt.type" + +export type BodyKompakt = PayloadKompakt & Omit diff --git a/packages/app/src/device/types/kompakt/header-kompakt.type.ts b/packages/app/src/device/types/kompakt/header-kompakt.type.ts new file mode 100644 index 0000000000..e6b44f62a3 --- /dev/null +++ b/packages/app/src/device/types/kompakt/header-kompakt.type.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + EndpointKompakt, + MethodKompakt, + ResponseStatus, +} from "App/device/constants" + +export type HeaderKompakt = { + endpoint: EndpointKompakt + method: MethodKompakt + offset: number + limit: number + uuid: number + status: ResponseStatus + contd: boolean +} diff --git a/packages/app/src/device/types/kompakt/payload-kompakt.type.ts b/packages/app/src/device/types/kompakt/payload-kompakt.type.ts new file mode 100644 index 0000000000..43e5fff553 --- /dev/null +++ b/packages/app/src/device/types/kompakt/payload-kompakt.type.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { BatteryStateKompakt } from "App/device/constants" +import { SimCardKompakt } from "App/device/types/kompakt/sim-card-kompakt.type" + +export type PayloadKompakt = { + serialNumber: string + version: number + batteryState: BatteryStateKompakt + batteryCapacity: number + simCards: SimCardKompakt[] + message?: string +} diff --git a/packages/app/src/device/types/kompakt/sim-card-kompakt.type.ts b/packages/app/src/device/types/kompakt/sim-card-kompakt.type.ts new file mode 100644 index 0000000000..9d7a9accdd --- /dev/null +++ b/packages/app/src/device/types/kompakt/sim-card-kompakt.type.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { NetworkStatusKompakt } from "App/device/constants" + +export type SimCardKompakt = { + simSlot: number + networkOperatorName: string + signalStrength: number + networkStatus: NetworkStatusKompakt +} diff --git a/packages/app/src/device/types/mudita-os/request-config.type.ts b/packages/app/src/device/types/mudita-os/request-config.type.ts index f8661a6026..829e1449b2 100644 --- a/packages/app/src/device/types/mudita-os/request-config.type.ts +++ b/packages/app/src/device/types/mudita-os/request-config.type.ts @@ -9,14 +9,20 @@ export interface RequestConfigOptions { connectionTimeOut?: number } -export interface RequestConfig { +export type Header = { endpoint: Endpoint method: Method + offset?: number + limit?: number +} + +export interface RequestConfig> extends Header { body?: Body filePath?: string options?: RequestConfigOptions } -export interface RequestPayload extends RequestConfig { +export interface RequestPayload> + extends RequestConfig { uuid: number } diff --git a/packages/app/src/device/types/mudita-os/serialport-request.type.ts b/packages/app/src/device/types/mudita-os/serialport-request.type.ts index 94229e35e7..e39ed87e64 100644 --- a/packages/app/src/device/types/mudita-os/serialport-request.type.ts +++ b/packages/app/src/device/types/mudita-os/serialport-request.type.ts @@ -98,6 +98,10 @@ export interface CreateContactRequestConfig extends RequestConfig { export type CreateContactResponseBody = Contact +export interface CreateContactErrorResponseBody { + duplicateNumbers: string[] +} + export interface UpdateContactRequestConfig extends RequestConfig { endpoint: Endpoint.Contacts method: Method.Put @@ -325,7 +329,7 @@ export interface Thread { threadID: number } -export interface Template { +export interface PureTemplate { templateID: number lastUsedAt: number templateBody: string @@ -376,7 +380,7 @@ export interface GetTemplatesRequestConfig } export interface GetTemplatesResponseBody { - entries: Template[] + entries: PureTemplate[] totalCount: number nextPage?: PaginationBody } diff --git a/packages/app/src/eula-agreement/components/agreement-modal/agreement-modal.component.tsx b/packages/app/src/eula-agreement/components/agreement-modal/agreement-modal.component.tsx index ef274ac501..2227a416e9 100644 --- a/packages/app/src/eula-agreement/components/agreement-modal/agreement-modal.component.tsx +++ b/packages/app/src/eula-agreement/components/agreement-modal/agreement-modal.component.tsx @@ -17,8 +17,7 @@ import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/mod import Text, { TextDisplayStyle, } from "App/__deprecated__/renderer/components/core/text/text.component" -import { zIndex } from "App/__deprecated__/renderer/styles/theming/theme-getters" -import theme from "App/__deprecated__/renderer/styles/theming/theme" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" const messages = defineMessages({ title: { @@ -34,8 +33,8 @@ export const AgreementModal: FunctionComponent = ({ }) => { return ( { + return fs.statSync(filePath).size + } + public async extractFile(filePath: string, cwd: string): Promise { const input = new stream.PassThrough() const file = this.readFile(filePath) diff --git a/packages/app/src/files-manager/actions/base.action.ts b/packages/app/src/files-manager/actions/base.action.ts index 77b68b1bfa..78fba8e3f9 100644 --- a/packages/app/src/files-manager/actions/base.action.ts +++ b/packages/app/src/files-manager/actions/base.action.ts @@ -42,3 +42,8 @@ export const setPendingFilesToUpload = createAction( export const setDuplicatedFiles = createAction( FilesManagerEvent.SetDuplicatedFiles ) +export const resetFiles = createAction(FilesManagerEvent.ResetFiles) + +export const setInvalidFiles = createAction( + FilesManagerEvent.SetInvalidFiles +) diff --git a/packages/app/src/files-manager/actions/continue-pending-upload.action.ts b/packages/app/src/files-manager/actions/continue-pending-upload.action.ts index 03d4f31744..d633dba940 100644 --- a/packages/app/src/files-manager/actions/continue-pending-upload.action.ts +++ b/packages/app/src/files-manager/actions/continue-pending-upload.action.ts @@ -37,7 +37,7 @@ export const continuePendingUpload = createAsyncThunk( } const harmonyFreeFilesSlotsCount = getHarmonyFreeFilesSlotsCount( - state.filesManager.files.length + state.filesManager.files?.length ?? 0 ) if ( @@ -72,12 +72,12 @@ export const continuePendingUpload = createAsyncThunk( directory, filePaths, }) + void dispatch(getFiles(directory)) if (!result.ok) { return rejectWithValue(result.error) } - void dispatch(getFiles(directory)) void dispatch(loadStorageInfoAction()) dispatch(setUploadingState(State.Loaded)) diff --git a/packages/app/src/files-manager/actions/get-files.action.ts b/packages/app/src/files-manager/actions/get-files.action.ts index c9112d4ed0..f5ef488055 100644 --- a/packages/app/src/files-manager/actions/get-files.action.ts +++ b/packages/app/src/files-manager/actions/get-files.action.ts @@ -7,7 +7,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { FilesManagerEvent, DeviceDirectory, - EligibleFormat, + eligibleFormat, } from "App/files-manager/constants" import { getFilesRequest } from "App/files-manager/requests/get-files.request" import { File } from "App/files-manager/dto" @@ -17,7 +17,7 @@ export const getFiles = createAsyncThunk( async (payload, { rejectWithValue }) => { const result = await getFilesRequest({ directory: payload, - filter: { extensions: Object.values(EligibleFormat) }, + filter: { extensions: eligibleFormat }, }) if (!result.ok || !result.data) { diff --git a/packages/app/src/files-manager/actions/select-items.action.ts b/packages/app/src/files-manager/actions/select-items.action.ts index b5209e9b2a..284a066786 100644 --- a/packages/app/src/files-manager/actions/select-items.action.ts +++ b/packages/app/src/files-manager/actions/select-items.action.ts @@ -13,7 +13,7 @@ export const selectAllItems = createAsyncThunk( // eslint-disable-next-line @typescript-eslint/require-await async (_, { getState }) => { const state = getState() as RootState & ReduxRootState - const fileIds = state.filesManager.files.map((file) => file.id) + const fileIds = state.filesManager.files?.map((file) => file.id) ?? [] return fileIds } diff --git a/packages/app/src/files-manager/actions/upload-file.action.test.ts b/packages/app/src/files-manager/actions/upload-file.action.test.ts index 3a3f098e9c..2b923b6213 100644 --- a/packages/app/src/files-manager/actions/upload-file.action.test.ts +++ b/packages/app/src/files-manager/actions/upload-file.action.test.ts @@ -12,27 +12,25 @@ import { Result, SuccessResult } from "App/core/builder" import { AppError } from "App/core/errors" import { pendingAction } from "App/__deprecated__/renderer/store/helpers" import { testError } from "App/__deprecated__/renderer/store/constants" -import { getPathsRequest } from "App/file-system/requests" import { uploadFilesRequest } from "App/files-manager/requests" -import { EligibleFormat } from "App/files-manager/constants/eligible-format.constant" import { DeviceDirectory } from "App/files-manager/constants/device-directory.constant" -import { GetPathsInput } from "App/file-system/dto" import { uploadFile } from "App/files-manager/actions/upload-file.action" import { setUploadBlocked, setUploadingFileCount, setUploadingState, } from "App/files-manager/actions/base.action" +import { getFiles } from "App/files-manager/actions/get-files.action" jest.mock("App/file-system/requests") jest.mock("App/files-manager/requests") -jest.mock("App/files-manager/actions/get-files.action", () => ({ - getFiles: jest.fn().mockReturnValue({ - type: pendingAction("FILES_MANAGER_GET_FILES"), - payload: undefined, - }), -})) +const GET_FILES_MOCK_RESULT = { + type: pendingAction("FILES_MANAGER_GET_FILES"), + payload: undefined, +} + +jest.mock("App/files-manager/actions/get-files.action") jest.mock("App/device/actions/load-storage-info.action", () => ({ loadStorageInfoAction: jest.fn().mockReturnValue({ @@ -43,8 +41,6 @@ jest.mock("App/device/actions/load-storage-info.action", () => ({ const pathsMock = ["/path/file-1.mp3", "/path/file-2.wav"] const errorMock = new AppError("SOME_ERROR_TYPE", "Luke, I'm your error") -const successGetPathResponse = new SuccessResult(pathsMock) -const failedGetPathResponse = Result.failed(errorMock) const successUploadResponse = new SuccessResult(pathsMock) const failedUploadResponse = Result.failed(errorMock) const initialStore = { @@ -56,101 +52,75 @@ const initialStore = { }, } -const getFilesPathsResponseMock: GetPathsInput = { - filters: [ - { - name: "Audio", - extensions: Object.values(EligibleFormat), - }, - ], - properties: ["openFile", "multiSelections"], -} - -describe("when `getPathRequest` request return Result.success with files list", () => { - describe("when `uploadFileRequest` request return Result.success with uploaded files list", () => { - beforeAll(() => { - ;(getPathsRequest as jest.Mock).mockResolvedValue(successGetPathResponse) - ;(uploadFilesRequest as jest.Mock).mockReturnValue(successUploadResponse) - }) +describe("when `uploadFileRequest` request return Result.success with uploaded files list", () => { + beforeAll(() => { + ;(uploadFilesRequest as jest.Mock).mockReturnValue(successUploadResponse) + ;(getFiles as unknown as jest.Mock).mockReturnValue(GET_FILES_MOCK_RESULT) + }) - afterEach(() => { - jest.resetAllMocks() - }) + afterEach(() => { + jest.resetAllMocks() + }) - test("dispatch `setUploadingState` with `State.Loaded` and `getFiles` with provided directory", async () => { - const mockStore = createMockStore([thunk])(initialStore) + test("dispatch `setUploadingState` with `State.Loaded` and `getFiles` with provided directory", async () => { + const mockStore = createMockStore([thunk])(initialStore) - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await mockStore.dispatch(uploadFile() as unknown as AnyAction) + const { + meta: { requestId }, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/await-thenable + } = await mockStore.dispatch(uploadFile(pathsMock) as unknown as AnyAction) - expect(mockStore.getActions()).toEqual([ - uploadFile.pending(requestId), - setUploadBlocked(true), - setUploadingFileCount(2), - setUploadingState(State.Loading), - setUploadBlocked(false), - { - type: pendingAction("FILES_MANAGER_GET_FILES"), - payload: undefined, - }, - { - type: pendingAction("DEVICE_LOAD_STORAGE_INFO"), - payload: undefined, - }, - - setUploadingState(State.Loaded), - uploadFile.fulfilled(undefined, requestId), - ]) + expect(mockStore.getActions()).toEqual([ + uploadFile.pending(requestId, pathsMock), + setUploadBlocked(true), + setUploadingFileCount(2), + setUploadingState(State.Loading), + GET_FILES_MOCK_RESULT, + { + type: pendingAction("DEVICE_LOAD_STORAGE_INFO"), + payload: undefined, + }, + setUploadingState(State.Loaded), + setUploadBlocked(false), + uploadFile.fulfilled(undefined, requestId, pathsMock), + ]) - expect(getPathsRequest).toHaveBeenLastCalledWith( - getFilesPathsResponseMock - ) - expect(uploadFilesRequest).toHaveBeenLastCalledWith({ - directory: DeviceDirectory.Music, - filePaths: pathsMock, - }) + expect(uploadFilesRequest).toHaveBeenLastCalledWith({ + directory: DeviceDirectory.Music, + filePaths: pathsMock, }) }) describe("when `uploadFileRequest` request return Result.success with empty files list", () => { - beforeAll(() => { - ;(getPathsRequest as jest.Mock).mockResolvedValue(Result.success([])) - }) - afterEach(() => { jest.resetAllMocks() }) - test("any action is dispatch", async () => { + test("Action is dispatched with empty array as a argument", async () => { const mockStore = createMockStore([thunk])(initialStore) const { meta: { requestId }, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/await-thenable - } = await mockStore.dispatch(uploadFile() as unknown as AnyAction) + } = await mockStore.dispatch(uploadFile([]) as unknown as AnyAction) expect(mockStore.getActions()).toEqual([ - uploadFile.pending(requestId), + uploadFile.pending(requestId, []), setUploadBlocked(true), setUploadBlocked(false), - uploadFile.fulfilled(undefined, requestId), + uploadFile.rejected(null, requestId, [], "no files to upload"), ]) - expect(getPathsRequest).toHaveBeenLastCalledWith( - getFilesPathsResponseMock - ) expect(uploadFilesRequest).not.toHaveBeenCalled() }) }) describe("when `uploadFileRequest` request return Result.failed", () => { - beforeAll(() => { - ;(getPathsRequest as jest.Mock).mockResolvedValue(successGetPathResponse) + beforeEach(() => { ;(uploadFilesRequest as jest.Mock).mockReturnValue(failedUploadResponse) + ;(getFiles as unknown as jest.Mock).mockReturnValue(GET_FILES_MOCK_RESULT) }) afterEach(() => { @@ -164,20 +134,20 @@ describe("when `getPathRequest` request return Result.success with files list", meta: { requestId }, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/await-thenable - } = await mockStore.dispatch(uploadFile() as unknown as AnyAction) + } = await mockStore.dispatch( + uploadFile(pathsMock) as unknown as AnyAction + ) expect(mockStore.getActions()).toEqual([ - uploadFile.pending(requestId), + uploadFile.pending(requestId, pathsMock), setUploadBlocked(true), setUploadingFileCount(2), setUploadingState(State.Loading), - setUploadBlocked(false), - uploadFile.rejected(testError, requestId, undefined, { ...errorMock }), + GET_FILES_MOCK_RESULT, + + uploadFile.rejected(testError, requestId, pathsMock, { ...errorMock }), ]) - expect(getPathsRequest).toHaveBeenLastCalledWith( - getFilesPathsResponseMock - ) expect(uploadFilesRequest).toHaveBeenLastCalledWith({ directory: DeviceDirectory.Music, filePaths: pathsMock, @@ -185,32 +155,3 @@ describe("when `getPathRequest` request return Result.success with files list", }) }) }) - -describe("when `getPathRequest` request return Result.failed", () => { - beforeAll(() => { - ;(getPathsRequest as jest.Mock).mockResolvedValue(failedGetPathResponse) - }) - - afterEach(() => { - jest.resetAllMocks() - }) - - test("failed with receive from `uploadFileRequest` error", async () => { - const mockStore = createMockStore([thunk])(initialStore) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await mockStore.dispatch(uploadFile() as unknown as AnyAction) - - expect(mockStore.getActions()).toEqual([ - uploadFile.pending(requestId), - setUploadBlocked(true), - uploadFile.rejected(testError, requestId, undefined, { ...errorMock }), - ]) - - expect(getPathsRequest).toHaveBeenLastCalledWith(getFilesPathsResponseMock) - expect(uploadFilesRequest).not.toHaveBeenCalled() - }) -}) diff --git a/packages/app/src/files-manager/actions/upload-file.action.ts b/packages/app/src/files-manager/actions/upload-file.action.ts index 30c8290d58..24a11a86ed 100644 --- a/packages/app/src/files-manager/actions/upload-file.action.ts +++ b/packages/app/src/files-manager/actions/upload-file.action.ts @@ -9,15 +9,14 @@ import { State } from "App/core/constants/state.constant" import { ReduxRootState } from "App/__deprecated__/renderer/store" import { FilesManagerEvent, - EligibleFormat, DeviceDirectory, FilesManagerError, } from "App/files-manager/constants" -import { getPathsRequest } from "App/file-system/requests" import { uploadFilesRequest } from "App/files-manager/requests" import { getFiles } from "App/files-manager/actions/get-files.action" import { setDuplicatedFiles, + setInvalidFiles, setPendingFilesToUpload, setUploadBlocked, setUploadingFileCount, @@ -28,44 +27,51 @@ import { getHarmonyFreeFilesSlotsCount } from "App/files-manager/helpers/get-fre import { getDuplicatedFiles } from "App/files-manager/helpers/get-duplicated-files.helper" import { getUniqueFiles } from "App/files-manager/helpers/get-unique-files.helper" import { AppError } from "App/core/errors/app-error" +import { checkFilesExtensions } from "../helpers/check-files-extensions.helper" -export const uploadFile = createAsyncThunk( +export const uploadFile = createAsyncThunk< + void, + string[], + { state: ReduxRootState } +>( FilesManagerEvent.UploadFiles, - async (_, { getState, dispatch, rejectWithValue }) => { - const state = getState() as ReduxRootState + async (filePaths, { getState, dispatch, rejectWithValue }) => { + dispatch(setUploadBlocked(true)) + + const state = getState() if (state.device.deviceType === null) { return rejectWithValue("device Type isn't set") } - dispatch(setUploadBlocked(true)) - const filesToUpload = await getPathsRequest({ - filters: [ - { - name: "Audio", - extensions: Object.values(EligibleFormat), - }, - ], - properties: ["openFile", "multiSelections"], - }) - if (!filesToUpload.ok || !filesToUpload.data) { - return rejectWithValue(filesToUpload.error) + if (state.filesManager.files === null) { + return rejectWithValue("files are not yet loaded") } - const filePaths = filesToUpload.data ?? [] + if (!filePaths || !filePaths.length) { + dispatch(setUploadBlocked(false)) + return rejectWithValue("no files to upload") + } + + const { validFiles, invalidFiles } = checkFilesExtensions(filePaths) - if (filePaths.length === 0) { + if (!validFiles.length && invalidFiles.length) { dispatch(setUploadBlocked(false)) - return + return rejectWithValue( + new AppError( + FilesManagerError.UnsupportedFileFormat, + "Unsupported file format" + ) + ) } const duplicatedFiles = getDuplicatedFiles( state.filesManager.files, - filePaths + validFiles ) if (duplicatedFiles.length > 0) { - const uniqueFiles = getUniqueFiles(state.filesManager.files, filePaths) + const uniqueFiles = getUniqueFiles(state.filesManager.files, validFiles) dispatch(setPendingFilesToUpload(uniqueFiles)) dispatch(setDuplicatedFiles(duplicatedFiles)) dispatch(setUploadBlocked(false)) @@ -78,7 +84,7 @@ export const uploadFile = createAsyncThunk( } const harmonyFreeFilesSlotsCount = getHarmonyFreeFilesSlotsCount( - state.filesManager.files.length + state.filesManager.files?.length ?? 0 ) if ( @@ -91,19 +97,18 @@ export const uploadFile = createAsyncThunk( if ( state.device.deviceType === DeviceType.MuditaHarmony && - harmonyFreeFilesSlotsCount < filePaths.length + harmonyFreeFilesSlotsCount < validFiles.length ) { dispatch( - setPendingFilesToUpload(filePaths.slice(0, harmonyFreeFilesSlotsCount)) + setPendingFilesToUpload(validFiles.slice(0, harmonyFreeFilesSlotsCount)) ) dispatch(setUploadingState(State.Pending)) dispatch(setUploadBlocked(false)) return } - dispatch(setUploadingFileCount(filePaths.length)) + dispatch(setUploadingFileCount(validFiles.length)) dispatch(setUploadingState(State.Loading)) - dispatch(setUploadBlocked(false)) const directory = state.device.deviceType === DeviceType.MuditaHarmony @@ -112,16 +117,21 @@ export const uploadFile = createAsyncThunk( const result = await uploadFilesRequest({ directory, - filePaths, + filePaths: validFiles, }) + void dispatch(getFiles(directory)) + if (!result.ok) { return rejectWithValue(result.error) } - void dispatch(getFiles(directory)) void dispatch(loadStorageInfoAction()) dispatch(setUploadingState(State.Loaded)) + dispatch(setUploadBlocked(false)) + if (invalidFiles.length) { + dispatch(setInvalidFiles(invalidFiles)) + } return } diff --git a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.test.tsx b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.test.tsx index b9231f87d4..a06a54da77 100644 --- a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.test.tsx +++ b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.test.tsx @@ -25,7 +25,6 @@ const defaultProps: FilesManagerPanelProps = { disabled: false, selectedFiles: [], allItemsSelected: false, - filesCount: 1, deviceType: DeviceType.MuditaPure, } @@ -33,6 +32,9 @@ const defaultState = { device: { deviceType: DeviceType.MuditaPure, }, + filesManager: { + files: [{}], + }, } as ReduxRootState const render = ( @@ -58,6 +60,9 @@ describe("When Mudita Harmony connected", () => { device: { deviceType: DeviceType.MuditaHarmony, }, + filesManager: { + files: [{}], + }, } as ReduxRootState) expect(queryByTestId(FilesManagerPanelTestIds.Wrapper)).toBeInTheDocument() @@ -70,6 +75,9 @@ describe("When Mudita Pure connected", () => { device: { deviceType: DeviceType.MuditaPure, }, + filesManager: { + files: [{}], + }, } as ReduxRootState) expect(queryByTestId(FilesManagerPanelTestIds.Wrapper)).toBeInTheDocument() diff --git a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.tsx b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.tsx index 80ce3f7dd2..d8e33a4bb9 100644 --- a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.tsx +++ b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.component.tsx @@ -26,6 +26,8 @@ import ElementWithTooltip from "App/__deprecated__/renderer/components/core/tool import styled from "styled-components" import { getHarmonyFreeFilesSlotsCount } from "App/files-manager/helpers/get-free-files-slots-count-for-harmony.helper" import { filesSlotsHarmonyLimit as filesSlotsHarmonyMaxLimit } from "App/files-manager/constants/files-slots-harmony-limit.constans" +import { useSelector } from "react-redux" +import { ReduxRootState } from "App/__deprecated__/renderer/store" const StyledTooltipPrimaryContent = styled(TooltipPrimaryContent)` max-width: 21rem; @@ -52,13 +54,15 @@ export const FilesManagerPanel: FunctionComponent = ({ resetRows, searchValue, onSearchValueChange, - filesCount, deviceType, }) => { const selectedItemsCount = selectedFiles.length const selectionMode = selectedItemsCount > 0 + const fileCount = + useSelector((state: ReduxRootState) => state.filesManager.files)?.length || + 0 - const filesSlotsHarmonyLimit = getHarmonyFreeFilesSlotsCount(filesCount) + const filesSlotsHarmonyLimit = getHarmonyFreeFilesSlotsCount(fileCount) const tooManyFiles = deviceType === DeviceType.MuditaHarmony && filesSlotsHarmonyLimit < 1 diff --git a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.interface.ts b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.interface.ts index d182d3734b..eae3fe3b73 100644 --- a/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.interface.ts +++ b/packages/app/src/files-manager/components/files-manager-panel/files-manager-panel.interface.ts @@ -15,6 +15,5 @@ export interface FilesManagerPanelProps { onDeleteClick: () => void selectedFiles: string[] allItemsSelected: boolean - filesCount: number deviceType: DeviceType } diff --git a/packages/app/src/files-manager/components/files-manager/files-manager.component.tsx b/packages/app/src/files-manager/components/files-manager/files-manager.component.tsx index a0f8e59953..d384839fc6 100644 --- a/packages/app/src/files-manager/components/files-manager/files-manager.component.tsx +++ b/packages/app/src/files-manager/components/files-manager/files-manager.component.tsx @@ -4,7 +4,7 @@ */ import { DeviceType } from "App/device/constants" -import React, { useEffect, useState } from "react" +import React, { useEffect, useRef, useState } from "react" import { State } from "App/core/constants" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import { FilesManagerContainer } from "App/files-manager/components/files-manager/files-manager.styled" @@ -18,6 +18,7 @@ import { FilesManagerTestIds } from "App/files-manager/components/files-manager/ import { DeviceDirectory, DiskSpaceCategoryType, + eligibleFormat, filesSummaryElements, } from "App/files-manager/constants" import FilesStorage from "App/files-manager/components/files-storage/files-storage.component" @@ -26,6 +27,10 @@ import { useLoadingState } from "App/ui" import { UploadFilesModals } from "App/files-manager/components/upload-files-modals/upload-files-modals.component" import { useFilesFilter } from "App/files-manager/helpers/use-files-filter.hook" import { getSpaces } from "App/files-manager/components/files-manager/get-spaces.helper" +import { useDispatch } from "react-redux" +import { resetFiles } from "App/files-manager/actions/base.action" +import { uploadFile } from "App/files-manager/actions" +import { Dispatch } from "App/__deprecated__/renderer/store" const FilesManager: FunctionComponent = ({ memorySpace = { @@ -38,7 +43,6 @@ const FilesManager: FunctionComponent = ({ deleting, files, getFiles, - uploadFile, deviceType, resetAllItems, selectAllItems, @@ -58,8 +62,9 @@ const FilesManager: FunctionComponent = ({ abortPendingUpload, continuePendingUpload, }) => { + const fileInputRef = useRef(null) const { noFoundFiles, searchValue, filteredFiles, handleSearchValueChange } = - useFilesFilter({ files }) + useFilesFilter({ files: files ?? [] }) const { states, updateFieldState } = useLoadingState({ deletingFailed: false, deleting: false, @@ -78,9 +83,10 @@ const FilesManager: FunctionComponent = ({ otherSpace, musicSpace, } = getSpaces(files, memorySpace) + const dispatch = useDispatch() + const dispatchThunk = useDispatch() const disableUpload = uploadBlocked ? uploadBlocked : freeSpace === 0 - const downloadFiles = () => { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/await-thenable @@ -91,6 +97,14 @@ const FilesManager: FunctionComponent = ({ } } + useEffect(() => { + return () => { + dispatch(resetFiles()) + } + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + useEffect(() => { if (deviceType) { void downloadFiles() @@ -175,9 +189,22 @@ const FilesManager: FunctionComponent = ({ }, [states.uploadingInfo]) useEffect(() => { - return () => resetUploadingState() + return () => { + resetUploadingState() + } }, [resetUploadingState]) + const onFileInputChange = () => { + if (fileInputRef.current?.files?.length) { + void dispatchThunk( + uploadFile( + Array.from(fileInputRef.current?.files).map((file) => file.path) + ) + ) + fileInputRef.current.value = "" + } + } + const getDiskSpaceCategories = (element: DiskSpaceCategory) => { const elements = { [DiskSpaceCategoryType.Free]: { @@ -191,7 +218,7 @@ const FilesManager: FunctionComponent = ({ [DiskSpaceCategoryType.Music]: { ...element, size: musicSpace, - filesAmount: files.length, + filesAmount: files?.length ?? 0, }, [DiskSpaceCategoryType.OtherSpace]: { ...element, @@ -201,11 +228,11 @@ const FilesManager: FunctionComponent = ({ return elements[element.type] } - const diskSpaceCategories: DiskSpaceCategory[] = filesSummaryElements.map( - (element) => { + const diskSpaceCategories: DiskSpaceCategory[] | null = + files && + filesSummaryElements.map((element) => { return getDiskSpaceCategories(element) - } - ) + }) const openDeleteModal = (ids: string[]) => { updateFieldState("deletingInfo", false) updateFieldState("uploadingInfo", false) @@ -239,6 +266,11 @@ const FilesManager: FunctionComponent = ({ setDeletingFileCount(0) resetDeletingState() } + + const handleUploadFiles = () => { + fileInputRef.current?.click() + } + return ( = ({ onCloseDeletingErrorModal={handleCloseDeletingErrorModal} onDelete={handleConfirmFilesDelete} /> - + {diskSpaceCategories && ( + + )} {deviceType !== null && ( = ({ toggleItem={toggleItem} onDeleteClick={handleDeleteClick} onManagerDeleteClick={handleManagerDeleteClick} - uploadFiles={uploadFile} + uploadFiles={handleUploadFiles} searchValue={searchValue} onSearchValueChange={handleSearchValueChange} noFoundFiles={noFoundFiles} @@ -288,6 +321,14 @@ const FilesManager: FunctionComponent = ({ deviceType={deviceType} /> )} + `audio/${format}`).join(",")} + hidden + multiple + onChange={onFileInputChange} + /> ) } diff --git a/packages/app/src/files-manager/components/files-manager/files-manager.interface.tsx b/packages/app/src/files-manager/components/files-manager/files-manager.interface.tsx index 46b5dd2426..dcfe1638c2 100644 --- a/packages/app/src/files-manager/components/files-manager/files-manager.interface.tsx +++ b/packages/app/src/files-manager/components/files-manager/files-manager.interface.tsx @@ -19,7 +19,7 @@ export interface FilesManagerProps { uploadingFileCount: number deleting: State deletingFileCount: number - files: File[] + files: File[] | null getFiles: (directory: DeviceDirectory) => void resetAllItems: () => void selectAllItems: () => void @@ -30,7 +30,6 @@ export interface FilesManagerProps { resetDeletingState: () => void resetUploadingState: () => void resetUploadingStateAfterSuccess: () => void - uploadFile: () => void uploadBlocked: boolean error: AppError | null setDeletingFileCount: (count: number) => void diff --git a/packages/app/src/files-manager/components/files-manager/files-manager.test.tsx b/packages/app/src/files-manager/components/files-manager/files-manager.test.tsx index d9eb29e2dd..200e1c11d3 100644 --- a/packages/app/src/files-manager/components/files-manager/files-manager.test.tsx +++ b/packages/app/src/files-manager/components/files-manager/files-manager.test.tsx @@ -32,7 +32,6 @@ const defaultProps: Props = { uploading: State.Initial, deleting: State.Initial, getFiles: jest.fn(), - uploadFile: jest.fn(), deviceType: DeviceType.MuditaPure, resetAllItems: jest.fn(), selectAllItems: jest.fn(), diff --git a/packages/app/src/files-manager/components/files-manager/get-spaces.helper.ts b/packages/app/src/files-manager/components/files-manager/get-spaces.helper.ts index 8693fb3479..81813ad6d5 100644 --- a/packages/app/src/files-manager/components/files-manager/get-spaces.helper.ts +++ b/packages/app/src/files-manager/components/files-manager/get-spaces.helper.ts @@ -20,11 +20,14 @@ export interface Spaces { * OS issue url https://appnroll.atlassian.net/browse/MOS-744 ***/ -export const getSpaces = (files: File[], memorySpace: MemorySpace): Spaces => { +export const getSpaces = ( + files: File[] | null, + memorySpace: MemorySpace +): Spaces => { const { reservedSpace, usedUserSpace, total } = memorySpace const usedMemorySpace = reservedSpace + usedUserSpace const freeSpace = total - usedMemorySpace - const musicSpace = files.reduce((a, b) => a + b.size, 0) + const musicSpace = files?.reduce((a, b) => a + b.size, 0) ?? 0 const otherSpace = usedUserSpace - musicSpace return { diff --git a/packages/app/src/files-manager/components/files-storage/files-storage.component.tsx b/packages/app/src/files-manager/components/files-storage/files-storage.component.tsx index 41ceaf5318..44ab27d438 100644 --- a/packages/app/src/files-manager/components/files-storage/files-storage.component.tsx +++ b/packages/app/src/files-manager/components/files-storage/files-storage.component.tsx @@ -66,7 +66,6 @@ const FilesStorage: FunctionComponent = ({ allItemsSelected={allItemsSelected} searchValue={searchValue} onSearchValueChange={onSearchValueChange} - filesCount={files.length} deviceType={deviceType} /> , state = defaultState) => { @@ -60,6 +63,9 @@ describe("`Files Storage` component", () => { device: { deviceType: DeviceType.MuditaPure, }, + filesManager: { + files: [{}], + }, } as ReduxRootState) expect(queryByTestId(FilesStorageTestIds.Title)).toHaveTextContent( "[value] component.filesManagerFilesStorageTitle" diff --git a/packages/app/src/files-manager/components/files-summary/files-summary.component.tsx b/packages/app/src/files-manager/components/files-summary/files-summary.component.tsx index cea8b25a4c..71e57321a6 100644 --- a/packages/app/src/files-manager/components/files-summary/files-summary.component.tsx +++ b/packages/app/src/files-manager/components/files-summary/files-summary.component.tsx @@ -23,6 +23,9 @@ import StackedBarChart, { } from "App/__deprecated__/renderer/components/core/stacked-bar-chart/stacked-bar-chart.component" import { defineMessages } from "react-intl" import { convertBytes } from "App/core/helpers/convert-bytes/convert-bytes" +import { DiskSpaceCategoryType } from "App/files-manager/constants/files-manager.enum" + +const otherCategoryLabel = DiskSpaceCategoryType.OtherSpace const FilesSummaryWrapper = styled.div` display: flex; @@ -40,7 +43,6 @@ interface Props { diskSpaceCategories: DiskSpaceCategory[] usedMemory: number totalMemorySpace: number - uploading: boolean } const memoryToStackedBarChartData = ( @@ -62,6 +64,19 @@ const FilesSummary: FunctionComponent = ({ }) => { const usedMemoryPercent = Math.floor((usedMemory / totalMemorySpace) * 100) + const otherCategory = React.useMemo(() => { + const otherCategory = diskSpaceCategories.find( + (category) => category.type === otherCategoryLabel + ) + return otherCategory + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const diskSpaceCategoriesToDisplay = diskSpaceCategories.map((category) => + category.type === otherCategory?.type ? otherCategory : category + ) + return ( = ({ message={messages.summaryTitle} /> - {diskSpaceCategories.map((diskSpaceCategory, index: number) => ( - - ))} + {diskSpaceCategoriesToDisplay.map( + (diskSpaceCategory, index: number) => ( + + ) + )} = ({ ) } -const shouldNotRerender = (_: Props, newProps: Props): boolean => { - return newProps.uploading -} - -export default React.memo(FilesSummary, shouldNotRerender) +export default FilesSummary diff --git a/packages/app/src/files-manager/components/files-summary/files-summary.stories.tsx b/packages/app/src/files-manager/components/files-summary/files-summary.stories.tsx index b75b81865e..a2bc90f10e 100644 --- a/packages/app/src/files-manager/components/files-summary/files-summary.stories.tsx +++ b/packages/app/src/files-manager/components/files-summary/files-summary.stories.tsx @@ -41,7 +41,6 @@ storiesOf("Views|Files Manager/Files Summary", module).add( usedMemory={62914560} totalMemorySpace={104857600} diskSpaceCategories={fakeData} - uploading={false} /> ) diff --git a/packages/app/src/files-manager/components/files-summary/files-summary.test.tsx b/packages/app/src/files-manager/components/files-summary/files-summary.test.tsx index 3e69113682..f2f849db80 100644 --- a/packages/app/src/files-manager/components/files-summary/files-summary.test.tsx +++ b/packages/app/src/files-manager/components/files-summary/files-summary.test.tsx @@ -38,7 +38,6 @@ const defaultProps: ComponentProps = { size: 4560, }, ], - uploading: false, } const render = () => { diff --git a/packages/app/src/files-manager/components/invalid-files-modal/invalid-files-modal.component.tsx b/packages/app/src/files-manager/components/invalid-files-modal/invalid-files-modal.component.tsx new file mode 100644 index 0000000000..79012e6626 --- /dev/null +++ b/packages/app/src/files-manager/components/invalid-files-modal/invalid-files-modal.component.tsx @@ -0,0 +1,97 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { defineMessages } from "react-intl" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { useDispatch, useSelector } from "react-redux" +import { getInvalidFiles } from "App/files-manager/selectors/get-invalid-files.selector" +import { + ModalContent, + ModalDialog, + RoundIconWrapper, +} from "App/ui/components/modal-dialog" +import React from "react" +import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" +import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import Text, { + TextDisplayStyle, +} from "App/__deprecated__/renderer/components/core/text/text.component" +import { resetUploadingState } from "App/files-manager/actions" +import styled from "styled-components" +import { + fontWeight, + textColor, +} from "App/__deprecated__/renderer/styles/theming/theme-getters" +import { ipcRenderer } from "electron-better-ipc" +import { HelpActions } from "App/__deprecated__/common/enums/help-actions.enum" + +const messages = defineMessages({ + title: { + id: "module.filesManager.invalidFiledModalTitle", + }, + filesInfo: { + id: "module.filesManager.invalidFiledModalFilesInfo", + }, + helpInfo: { + id: "module.filesManager.invalidFiledModalHelpInfo", + }, +}) + +const StyledLink = styled.a` + text-decoration: underline; + cursor: pointer; + font-size: 1.4rem; + font-weight: ${fontWeight("default")}; + color: ${textColor("action")}; +` +const StyledModalContent = styled(ModalContent)` + p { + text-align: left; + } +` + +export const InvalidFilesModal: FunctionComponent = ({ ...props }) => { + const invalidFiles = useSelector(getInvalidFiles) + const openHelpWindow = () => ipcRenderer.callMain(HelpActions.OpenWindow) + + const dispatch = useDispatch() + return ( + { + dispatch(resetUploadingState()) + }} + {...props} + > + + + + + + help pages + ), + }, + }} + /> + + + ) +} diff --git a/packages/app/src/files-manager/components/pending-upload-modal/pending-upload-modal.component.tsx b/packages/app/src/files-manager/components/pending-upload-modal/pending-upload-modal.component.tsx index 5c45a0f9b3..8e29678e8f 100644 --- a/packages/app/src/files-manager/components/pending-upload-modal/pending-upload-modal.component.tsx +++ b/packages/app/src/files-manager/components/pending-upload-modal/pending-upload-modal.component.tsx @@ -22,6 +22,7 @@ import styled from "styled-components" import { fontWeight } from "App/__deprecated__/renderer/styles/theming/theme-getters" import { PendingUploadModalProps } from "App/files-manager/components/pending-upload-modal/pending-upload-modal.interface" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { getModalButtonsSize } from "App/__deprecated__/renderer/components/core/modal/modal.helpers" const messages = defineMessages({ pendingUploadModalTitle: { @@ -39,11 +40,13 @@ const messages = defineMessages({ pendingUploadModalActionButton: { id: "module.filesManager.pendingUploadModalActionButton", }, + pendingUploadModalAbortButton: { + id: "module.filesManager.pendingUploadModalAbortButtonText", + }, }) const PendingUploadDetailText = styled(Text)` font-weight: ${fontWeight("default")}; - width: 25rem; ` const PendingUploadModal: FunctionComponent = ({ @@ -57,7 +60,7 @@ const PendingUploadModal: FunctionComponent = ({ size={ModalSize.Small} title={intl.formatMessage(messages.pendingUploadModalTitle)} open - closeButton={false} + closeButton actionButtonLabel={intl.formatMessage( messages.pendingUploadModalActionButton )} @@ -67,6 +70,13 @@ const PendingUploadModal: FunctionComponent = ({ closeModal={() => { onClose() }} + onCloseButton={() => { + onClose() + }} + closeButtonLabel={intl.formatMessage( + messages.pendingUploadModalAbortButton + )} + actionButtonSize={getModalButtonsSize(ModalSize.Small)} {...props} > diff --git a/packages/app/src/files-manager/components/unsupported-file-format-modal/unsupported-file-format-modal.component.tsx b/packages/app/src/files-manager/components/unsupported-file-format-modal/unsupported-file-format-modal.component.tsx new file mode 100644 index 0000000000..853f772cff --- /dev/null +++ b/packages/app/src/files-manager/components/unsupported-file-format-modal/unsupported-file-format-modal.component.tsx @@ -0,0 +1,89 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + ModalContent, + ModalDialog, + RoundIconWrapper, +} from "App/ui/components/modal-dialog" +import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import { defineMessages } from "react-intl" +import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" + +import React from "react" +import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import Text, { + TextDisplayStyle, +} from "App/__deprecated__/renderer/components/core/text/text.component" +import styled from "styled-components" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { useSelector, useDispatch } from "react-redux" +import { resetUploadingState } from "App/files-manager/actions/base.action" +import { getFilesManagerError } from "App/files-manager/selectors/get-files-manager-error.selector" +import { FilesManagerError } from "App/files-manager/constants/errors.enum" + +const messages = defineMessages({ + unsupportedFileFormatUploadModalTitle: { + id: "module.filesManager.unsupportedFileFormatUploadModalTitle", + }, + unsupportedFileFormatUploadModalHeader: { + id: "module.filesManager.unsupportedFileFormatUploadModalHeader", + }, + unsupportedFileFormatUploadModalTextInfo: { + id: "module.filesManager.unsupportedFileFormatUploadModalTextInfo", + }, + unsupportedFileFormatUploadModalActionButton: { + id: "module.filesManager.unsupportedFileFormatUploadModalActionButton", + }, +}) + +const DuplicatedFilesDetailText = styled(Text)` + margin-top: 0.8rem; +` + +const UnsupportedFileFormatModal: FunctionComponent = ({ ...props }) => { + const filesManagerError = useSelector(getFilesManagerError) + + const dispatch = useDispatch() + + if (filesManagerError?.type !== FilesManagerError.UnsupportedFileFormat) { + return null + } + + return ( + { + dispatch(resetUploadingState()) + }} + {...props} + > + + + + + + + + + ) +} + +export default UnsupportedFileFormatModal diff --git a/packages/app/src/files-manager/components/unsupported-file-size-modal/unsupported-file-size-modal.component.tsx b/packages/app/src/files-manager/components/unsupported-file-size-modal/unsupported-file-size-modal.component.tsx new file mode 100644 index 0000000000..0a33aa4f8a --- /dev/null +++ b/packages/app/src/files-manager/components/unsupported-file-size-modal/unsupported-file-size-modal.component.tsx @@ -0,0 +1,91 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + ModalContent, + ModalDialog, + RoundIconWrapper, +} from "App/ui/components/modal-dialog" +import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import { defineMessages } from "react-intl" +import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" + +import React from "react" +import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import Text, { + TextDisplayStyle, +} from "App/__deprecated__/renderer/components/core/text/text.component" +import styled from "styled-components" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { useSelector, useDispatch } from "react-redux" +import { resetUploadingState } from "App/files-manager/actions/base.action" +import { getFilesManagerError } from "App/files-manager/selectors/get-files-manager-error.selector" +import { FilesManagerError } from "App/files-manager/constants/errors.enum" + +const messages = defineMessages({ + unsupportedFileSizeFilesUploadModalTitle: { + id: "module.filesManager.unsupportedFileSizeFilesUploadModalTitle", + }, + unsupportedFileSizeFilesUploadModalHeader: { + id: "module.filesManager.unsupportedFileSizeFilesUploadModalHeader", + }, + unsupportedFileSizeFilesUploadModalTextInfo: { + id: "module.filesManager.unsupportedFileSizeFilesUploadModalTextInfo", + }, + unsupportedFileSizeFilesUploadModalActionButton: { + id: "module.filesManager.unsupportedFileSizeFilesUploadModalActionButton", + }, +}) + +const UnsupportedFileSizeDetailText = styled(Text)` + margin-top: 0.8rem; +` + +const UnsupportedFileSizeModal: FunctionComponent = ({ ...props }) => { + const filesManagerError = useSelector(getFilesManagerError) + + const dispatch = useDispatch() + + if (filesManagerError?.type !== FilesManagerError.UnsupportedFileSize) { + return null + } + + return ( + { + dispatch(resetUploadingState()) + }} + {...props} + > + + + + + + + + + ) +} + +export default UnsupportedFileSizeModal diff --git a/packages/app/src/files-manager/components/upload-files-modals/upload-files-modals.component.tsx b/packages/app/src/files-manager/components/upload-files-modals/upload-files-modals.component.tsx index 4aaf0d9117..b5bcd78574 100644 --- a/packages/app/src/files-manager/components/upload-files-modals/upload-files-modals.component.tsx +++ b/packages/app/src/files-manager/components/upload-files-modals/upload-files-modals.component.tsx @@ -15,6 +15,9 @@ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-co import { intl, textFormatters } from "App/__deprecated__/renderer/utils/intl" import PendingUploadModal from "App/files-manager/components/pending-upload-modal/pending-upload-modal.component" import DuplicatedFilesModal from "App/files-manager/components/duplicated-files-modal/duplicated-files-modal.component" +import UnsupportedFileFormatModal from "App/files-manager/components/unsupported-file-format-modal/unsupported-file-format-modal.component" +import UnsupportedFileSizeModal from "App/files-manager/components/unsupported-file-size-modal/unsupported-file-size-modal.component" +import { InvalidFilesModal } from "../invalid-files-modal/invalid-files-modal.component" const messages = defineMessages({ uploadingModalInfo: { id: "module.filesManager.uploadingModalInfo" }, @@ -84,7 +87,9 @@ export const UploadFilesModals: FunctionComponent = ({ /> )} {uploadingFailed && - error?.type !== FilesManagerError.UploadDuplicates && ( + error?.type !== FilesManagerError.UploadDuplicates && + error?.type !== FilesManagerError.UnsupportedFileFormat && + error?.type !== FilesManagerError.UnsupportedFileSize && ( = ({ /> )} + + + ) } diff --git a/packages/app/src/files-manager/constants/eligible-format.constant.ts b/packages/app/src/files-manager/constants/eligible-format.constant.ts index 04c3e0aaa4..da7793b5b5 100644 --- a/packages/app/src/files-manager/constants/eligible-format.constant.ts +++ b/packages/app/src/files-manager/constants/eligible-format.constant.ts @@ -3,8 +3,4 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export enum EligibleFormat { - MP3 = "mp3", - WAV = "wav", - FLAC = "flac", -} +export const eligibleFormat = ["mp3", "wav", "flac"] diff --git a/packages/app/src/files-manager/constants/errors.enum.ts b/packages/app/src/files-manager/constants/errors.enum.ts index 246e95829c..269b365743 100644 --- a/packages/app/src/files-manager/constants/errors.enum.ts +++ b/packages/app/src/files-manager/constants/errors.enum.ts @@ -9,4 +9,6 @@ export enum FilesManagerError { DeleteFiles = "FILES_MANAGER_DELETE_FILES_ERROR", NotEnoughSpace = "FILES_MANGER_NOT_ENOUGH_SPACE_ERROR", UploadDuplicates = "FILES_MANGER_UPLOAD_DUPLICATES", + UnsupportedFileFormat = "FILES_MANGER_UNSUPPORTED_FILE_FORMAT", + UnsupportedFileSize = "FILES_MANAGER_UNSUPPORTED_FILE_SIZE_ERROR", } diff --git a/packages/app/src/files-manager/constants/event.enum.ts b/packages/app/src/files-manager/constants/event.enum.ts index cc700c4e07..abdc6c1362 100644 --- a/packages/app/src/files-manager/constants/event.enum.ts +++ b/packages/app/src/files-manager/constants/event.enum.ts @@ -22,4 +22,6 @@ export enum FilesManagerEvent { AbortPendingUpload = "FILES_MANAGER_ABORT_PENDING_UPLOAD", ContinuePendingUpload = "FILES_MANAGER_CONTINUE_PENDING_UPLOAD", SetDuplicatedFiles = "FILES_MANAGER_SET_DUPLICATED_FILES", + SetInvalidFiles = "FILES_MANAGER_SET_INVALID_FILES", + ResetFiles = "FILES_MANAGER_RESET_FILES", } diff --git a/packages/app/src/files-manager/files-manager.container.tsx b/packages/app/src/files-manager/files-manager.container.tsx index 84b29c169d..e326793d63 100644 --- a/packages/app/src/files-manager/files-manager.container.tsx +++ b/packages/app/src/files-manager/files-manager.container.tsx @@ -11,7 +11,6 @@ import { selectAllItems, toggleItem, getFiles, - uploadFile, resetDeletingState, resetUploadingState, setDeletingFileCount, @@ -34,7 +33,7 @@ const mapStateToProps = (state: RootState & ReduxRootState) => ({ selectedItems: state.filesManager.selectedItems.rows, allItemsSelected: state.filesManager.selectedItems.rows.length === - state.filesManager.files.length, + (state.filesManager.files?.length ?? 0), uploadBlocked: state.filesManager.uploadBlocked, pendingFilesCount: state.filesManager.uploadPendingFiles.length, }) @@ -44,7 +43,6 @@ const mapDispatchToProps = { resetAllItems, selectAllItems, toggleItem, - uploadFile, deleteFiles, resetDeletingState, resetUploadingState, diff --git a/packages/app/src/files-manager/helpers/check-files-extensions.helper.test.ts b/packages/app/src/files-manager/helpers/check-files-extensions.helper.test.ts new file mode 100644 index 0000000000..5cb2803847 --- /dev/null +++ b/packages/app/src/files-manager/helpers/check-files-extensions.helper.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { checkFilesExtensions } from "App/files-manager/helpers/check-files-extensions.helper" +import { eligibleFormat } from "App/files-manager/constants/eligible-format.constant" + +const supportedFiles = eligibleFormat.map((extension) => `file.${extension}`) + +describe("`checkFilesExtensions` helper", () => { + test("all correct extensions", () => { + expect(checkFilesExtensions(supportedFiles)).toEqual({ + validFiles: supportedFiles, + invalidFiles: [], + }) + }) + + test("empty files array", () => { + expect(checkFilesExtensions([])).toEqual({ + validFiles: [], + invalidFiles: [], + }) + }) + + test("at least one unsupported extension", () => { + const unsupportedFiles = ["file.unsupported"] + const allFiles = [...supportedFiles, ...unsupportedFiles] + expect(checkFilesExtensions(allFiles)).toEqual({ + validFiles: supportedFiles, + invalidFiles: unsupportedFiles, + }) + }) +}) diff --git a/packages/app/src/files-manager/helpers/check-files-extensions.helper.ts b/packages/app/src/files-manager/helpers/check-files-extensions.helper.ts new file mode 100644 index 0000000000..5f22969c43 --- /dev/null +++ b/packages/app/src/files-manager/helpers/check-files-extensions.helper.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { eligibleFormat } from "App/files-manager/constants/eligible-format.constant" + +export const checkFilesExtensions = ( + filesPaths: string[] +): { validFiles: string[]; invalidFiles: string[] } => { + const isPathEligible = (path: string) => + eligibleFormat.includes((path.split(".").pop() ?? "").toLocaleLowerCase()) + const validFiles = filesPaths.filter((filePath) => isPathEligible(filePath)) + const invalidFiles = filesPaths.filter( + (filePath) => !isPathEligible(filePath) + ) + + return { validFiles, invalidFiles } +} diff --git a/packages/app/src/files-manager/helpers/get-duplicated-files.helper.ts b/packages/app/src/files-manager/helpers/get-duplicated-files.helper.ts index e9fa7bb2bc..5ff4ddece4 100644 --- a/packages/app/src/files-manager/helpers/get-duplicated-files.helper.ts +++ b/packages/app/src/files-manager/helpers/get-duplicated-files.helper.ts @@ -7,15 +7,16 @@ import { intersection } from "lodash" import { File } from "App/files-manager/dto" export const getDuplicatedFiles = ( - files: File[], + files: File[] | null, filesPaths: string[] ): string[] => { const filesNamesFromPaths = filesPaths.map((filePath) => { return filePath.split(/\\|\//g).reverse()[0] }) - const filesNamesFromFiles = files.map((file) => { - return file.name - }) + const filesNamesFromFiles = + files?.map((file) => { + return file.name + }) ?? [] return intersection(filesNamesFromFiles, filesNamesFromPaths) } diff --git a/packages/app/src/files-manager/helpers/get-unique-files.helper.ts b/packages/app/src/files-manager/helpers/get-unique-files.helper.ts index eb85ee3863..1da648107e 100644 --- a/packages/app/src/files-manager/helpers/get-unique-files.helper.ts +++ b/packages/app/src/files-manager/helpers/get-unique-files.helper.ts @@ -7,15 +7,16 @@ import { difference } from "lodash" import { File } from "App/files-manager/dto" export const getUniqueFiles = ( - files: File[], + files: File[] | null, filesPaths: string[] ): string[] => { const filesNamesFromPaths = filesPaths.map((filePath) => { return filePath.split(/\\|\//g).reverse()[0] }) - const filesNamesFromFiles = files.map((file) => { - return file.name - }) + const filesNamesFromFiles = + files?.map((file) => { + return file.name + }) ?? [] return difference(filesNamesFromPaths, filesNamesFromFiles) } diff --git a/packages/app/src/files-manager/reducers/files-manager.interface.ts b/packages/app/src/files-manager/reducers/files-manager.interface.ts index e9c6355376..958136e0db 100644 --- a/packages/app/src/files-manager/reducers/files-manager.interface.ts +++ b/packages/app/src/files-manager/reducers/files-manager.interface.ts @@ -8,7 +8,7 @@ import { State } from "App/core/constants" import { File } from "App/files-manager/dto" export interface FilesManagerState { - files: File[] + files: File[] | null loading: State deleting: State uploading: State @@ -19,4 +19,5 @@ export interface FilesManagerState { uploadBlocked: boolean uploadPendingFiles: string[] duplicatedFiles: string[] + invalidFiles: string[] } diff --git a/packages/app/src/files-manager/reducers/files-manager.reducer.ts b/packages/app/src/files-manager/reducers/files-manager.reducer.ts index 197dd61c26..c95ddf3b14 100644 --- a/packages/app/src/files-manager/reducers/files-manager.reducer.ts +++ b/packages/app/src/files-manager/reducers/files-manager.reducer.ts @@ -21,6 +21,8 @@ import { setPendingFilesToUpload, setDuplicatedFiles, resetUploadingStateAfterSuccess, + resetFiles, + setInvalidFiles, } from "App/files-manager/actions" import { changeLocation } from "App/core/actions" import { FilesManagerState } from "App/files-manager/reducers/files-manager.interface" @@ -28,7 +30,7 @@ import { deleteFiles } from "App/files-manager/actions/delete-files.action" import { continuePendingUpload } from "../actions/continue-pending-upload.action" export const initialState: FilesManagerState = { - files: [], + files: null, loading: State.Initial, uploading: State.Initial, deleting: State.Initial, @@ -41,6 +43,7 @@ export const initialState: FilesManagerState = { error: null, uploadPendingFiles: [], duplicatedFiles: [], + invalidFiles: [], } export const filesManagerReducer = createReducer( @@ -51,6 +54,7 @@ export const filesManagerReducer = createReducer( return { ...state, loading: State.Loading, + error: null, } }) .addCase(getFiles.fulfilled, (state, action) => { @@ -58,7 +62,6 @@ export const filesManagerReducer = createReducer( ...state, loading: State.Loaded, files: action.payload, - error: null, } }) .addCase(getFiles.rejected, (state, action) => { @@ -82,6 +85,12 @@ export const filesManagerReducer = createReducer( } }) .addCase(uploadFile.rejected, (state, action) => { + if (action.payload === undefined) { + return { + ...state, + } + } + return { ...state, uploading: State.Failed, @@ -134,9 +143,12 @@ export const filesManagerReducer = createReducer( .addCase(deleteFiles.fulfilled, (state, action) => { return { ...state, - files: [...state.files].filter( - (file) => !action.payload.some((id) => id === file.id) - ), + files: + state.files === null + ? null + : [...state.files].filter( + (file) => !action.payload.some((id) => id === file.id) + ), deleting: State.Loaded, error: null, } @@ -163,6 +175,7 @@ export const filesManagerReducer = createReducer( uploadingFileCount: 0, uploadBlocked: false, duplicatedFiles: [], + invalidFiles: [], } }) .addCase(resetUploadingStateAfterSuccess, (state) => { @@ -193,5 +206,11 @@ export const filesManagerReducer = createReducer( .addCase(setDuplicatedFiles, (state, action) => { state.duplicatedFiles = action.payload }) + .addCase(setInvalidFiles, (state, action) => { + state.invalidFiles = action.payload + }) + .addCase(resetFiles, (state, _) => { + state.files = null + }) } ) diff --git a/packages/app/src/files-manager/selectors/get-invalid-files.selector.ts b/packages/app/src/files-manager/selectors/get-invalid-files.selector.ts new file mode 100644 index 0000000000..8439bc6a50 --- /dev/null +++ b/packages/app/src/files-manager/selectors/get-invalid-files.selector.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createSelector } from "@reduxjs/toolkit" +import { getFilesManager } from "App/files-manager/selectors/get-files-manager.selector" + +export const getInvalidFiles = createSelector( + getFilesManager, + (filesManager): string[] => { + return filesManager.invalidFiles + } +) diff --git a/packages/app/src/files-manager/services/file-manager.service.ts b/packages/app/src/files-manager/services/file-manager.service.ts index 5c1ed3818a..bb59956c4b 100644 --- a/packages/app/src/files-manager/services/file-manager.service.ts +++ b/packages/app/src/files-manager/services/file-manager.service.ts @@ -54,30 +54,38 @@ export class FileManagerService { directory, filePaths, }: UploadFilesInput): Promise> { - const results = [] - - for await (const filePath of filePaths) { - results.push(await this.fileUploadCommand.exec(directory, filePath)) - } - - const success = results.every((result) => result.ok) - const noSpaceLeft = results.some( - (result) => result.error?.type === DeviceFileSystemError.NoSpaceLeft - ) - - if (noSpaceLeft) { - return Result.failed( - new AppError( - FilesManagerError.NotEnoughSpace, - "Not enough space on your device" + for (const filePath of filePaths) { + try { + const { error, ok } = await this.fileUploadCommand.exec( + directory, + filePath ) - ) - } - - if (!success) { - return Result.failed( - new AppError(FilesManagerError.UploadFiles, "Upload failed") - ) + if (error?.type === DeviceFileSystemError.NoSpaceLeft) { + return Result.failed( + new AppError( + FilesManagerError.NotEnoughSpace, + "Not enough space on your device" + ) + ) + } + if (error?.type === DeviceFileSystemError.UnsupportedFileSize) { + return Result.failed( + new AppError( + FilesManagerError.UnsupportedFileSize, + "Unsupported file size" + ) + ) + } + if (!ok) { + return Result.failed( + new AppError(FilesManagerError.UploadFiles, "Upload failed") + ) + } + } catch (error) { + return Result.failed( + new AppError(FilesManagerError.UploadFiles, "Upload failed") + ) + } } return Result.success(filePaths) @@ -86,18 +94,13 @@ export class FileManagerService { public async deleteFiles( filePaths: string[] ): Promise> { - const results = [] - - for await (const filePath of filePaths) { - results.push(await this.fileDeleteCommand.exec(filePath)) - } - - const success = results.every((result) => result.ok) - - if (!success) { - return Result.failed( - new AppError(FilesManagerError.DeleteFiles, "Delete failed") - ) + for (const filePath of filePaths) { + const { ok } = await this.fileDeleteCommand.exec(filePath) + if (!ok) { + return Result.failed( + new AppError(FilesManagerError.DeleteFiles, "Delete failed") + ) + } } return Result.success(filePaths) diff --git a/packages/app/src/help/default-help.json b/packages/app/src/help/default-help.json new file mode 100644 index 0000000000..7640568648 --- /dev/null +++ b/packages/app/src/help/default-help.json @@ -0,0 +1 @@ +{"collection":["3gHVtYGoa4SOixNHTxnSlJ","5JSK4d3iUXoWNOGjj1lTTF","24oHmzMGKaqoWKKFjX3mJT","DwCnLFElFKxV8yz7HjqKT","60AtHCFNpdJ7jA26bC55ju","m9GNSfDdpvSeQbqgXVHQ6","7laaEbQCtg7hfQjZIqO7Lv","3El0ssG9bX6jxvDDckWQiP","5z0tuCdYhnnQrRyAsNoAHT","kZ3ZB95I8tCX0lZYQTRZr","1Cr7VOps6vyYUgw2VwDJSU","RxG8PskOtkWQ00aCL4JJ7","6rTLyyR6Khdu6d4GGpcbon","68ZLLjHM25XQKu9pRLUtPR","GSBqsntyLST2hANzSdMh1","1OzihaTKRPEfPk3NPyRhku","4kQ3BfvtPxoM1ErBSoXlMW","2W0Yh5enrN1SqWGkvGkvf2","3UzcSgFRo8CjfMUTycu3KB","5D42R98KntZbQUTcrhizxs","4Kj1OMtlPJSTs27TkS9ELG","1oCmuEnSQyYHsSvew1g0vm"],"items":{"3gHVtYGoa4SOixNHTxnSlJ":{"id":"3gHVtYGoa4SOixNHTxnSlJ","question":"OS update failed","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Make sure you didn’t any of the following during the update:","marks":[],"data":{}}],"data":{}},{"nodeType":"unordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"use your Mudita Pure,","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"unplug your Mudita Pure from the computer,","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"turn off your Mudita Pure,","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"close the Mudita Center app.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Still not working? Contact our Support ","marks":[],"data":{}},{"nodeType":"hyperlink","content":[{"nodeType":"text","value":"support@mudita.com","marks":[{"type":"underline"}],"data":{}}],"data":{"uri":"mailto:support@mudita.com"}},{"nodeType":"text","value":" ","marks":[],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"5JSK4d3iUXoWNOGjj1lTTF":{"id":"5JSK4d3iUXoWNOGjj1lTTF","question":"How to delete all contacts?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Contacts” section inside the “YOUR PURE” section.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Select a contact you want to delete.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"On the top bar above click on the check box (near inscription 1 Contact selected) by using the search bar.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Once all contacts are selected, click “Delete” in the search bar and confirm the action.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"All selected contacts will be deleted.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"24oHmzMGKaqoWKKFjX3mJT":{"id":"24oHmzMGKaqoWKKFjX3mJT","question":"How to export my Contacts to the file? ","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Contacts” section inside the “YOUR PURE” section.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Select the contacts you want to export.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click the “Export” icon at the top bar.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Choose the file you want your contacts exported to. ","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You will be able to see the confirmation that contacts have been exported to the file.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"DwCnLFElFKxV8yz7HjqKT":{"id":"DwCnLFElFKxV8yz7HjqKT","question":"How to update Mudita Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Run Mudita Center on your computer and make sure you have an Internet connection.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"If an update is available, Mudita Center will show you the relevant information with an option to update the software.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click the “update now” button and follow the instructions.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"To check for an update manually, head into Settings > About > Check for Updates.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"After a successful update, Mudita Center will restart.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set, after the app reopens & you’re able to use the latest version.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"60AtHCFNpdJ7jA26bC55ju":{"id":"60AtHCFNpdJ7jA26bC55ju","question":"Forgot passcode?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Resetting your passcode is only available through a factory reset of your Mudita Pure.","marks":[],"data":{}}],"data":{}}]}},"m9GNSfDdpvSeQbqgXVHQ6":{"id":"m9GNSfDdpvSeQbqgXVHQ6","question":"How to update Harmony OS?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Harmony to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Overview” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"In the sub-section showing your MuditaOS version, you will you will see a notification about whether there is a new software update available for your Mudita Harmony.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click the “Download Now” button and follow the instructions.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"After a successful update, your Harmony will automatically restart. After that, you will be able to see your MuditaOS version in the “Overview” section.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Note: Remember to not unplug your device from the computer during the update process","marks":[{"type":"bold"}],"data":{}},{"nodeType":"text","value":". ","marks":[],"data":{}}],"data":{}}]}},"7laaEbQCtg7hfQjZIqO7Lv":{"id":"7laaEbQCtg7hfQjZIqO7Lv","question":"How to check my phone's serial number?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Overview” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Under the photo of your Pure, just below the ","marks":[],"data":{}},{"nodeType":"text","value":"Disconnect ","marks":[{"type":"bold"}],"data":{}},{"nodeType":"text","value":"button you will see a clickable ‘About your Pure’ text.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Once inside, in the About your Pure section you will see your Serial number as well as additional information about SAR","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"3El0ssG9bX6jxvDDckWQiP":{"id":"3El0ssG9bX6jxvDDckWQiP","question":"How to send a message using Mudita Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Messages” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"On the right side of the search bar you will see a button.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Search for your desired contact or jump into an existing conversation.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set!","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"5z0tuCdYhnnQrRyAsNoAHT":{"id":"5z0tuCdYhnnQrRyAsNoAHT","question":"Where can I find the backup file to restore Pure?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Settings” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":" In the sub-section titled “Backup” navigate to ","marks":[],"data":{}},{"nodeType":"text","value":"Backup Location","marks":[{"type":"bold"}],"data":{}},{"nodeType":"text","value":", you will see the exact file path.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Follow the path on your PC in order to find the correct folder.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set!","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"kZ3ZB95I8tCX0lZYQTRZr":{"id":"kZ3ZB95I8tCX0lZYQTRZr","question":"How to change the backup’s hard drive location?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Settings” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"In the sub-section named \"Backup\" there will be an option visible to change the hard drive location.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click Change Location and select the desired folder.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set!","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"1Cr7VOps6vyYUgw2VwDJSU":{"id":"1Cr7VOps6vyYUgw2VwDJSU","question":"How to backup your Mudita Pure?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Overview” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"In the sub-section titled “Backup” you will see an option to either create your first backup file or retrieve a previously created backup file.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click twice and create a password in order to start creating the backup. This password is unique for each restoring process and cannot be restored.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set!","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"RxG8PskOtkWQ00aCL4JJ7":{"id":"RxG8PskOtkWQ00aCL4JJ7","question":"How to restore my Pure’s backup through Mudita Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your computer and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Overview” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"In the sub-section titled “Backup” there will be an option visible to retrieve or create your first backup file.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click and select the file you want to restore.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click Restore, enter the password you created when creating the backup file, and wait for the process to complete.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"6rTLyyR6Khdu6d4GGpcbon":{"id":"6rTLyyR6Khdu6d4GGpcbon","question":"How to import my iCloud contacts to Mudita Pure?","answer":{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[{"type":"bold"}],"value":"Note: Please note there is no direct option to sync Pure contacts with iCloud, there is an import from a file. ","nodeType":"text"}],"nodeType":"paragraph"},{"data":{},"content":[{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Go to the “Contacts” section inside the “YOUR PURE” section.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Click on the “Import” button.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Click “Import from ICS file”, it will direct you to the system modal which allows you to choose a file from your computer. Make sure that you have exported your iCloud contact into an ICS file.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Once the file is chosen to import, you will be able to see the list of the contacts (you can choose contacts to import).","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Click the confirmation button to start the import process.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"After the process is finished successfully you will be able to view the contacts in Mudita Center and on Mudita Pure","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"}],"nodeType":"ordered-list"},{"data":{},"content":[{"data":{},"marks":[],"value":"","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"document"}},"68ZLLjHM25XQKu9pRLUtPR":{"id":"68ZLLjHM25XQKu9pRLUtPR","question":"How to update MuditaOS in Mudita Pure?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your computer with a dedicated USB cable and make sure that Mudita Center runs on your machine and recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Overview” section inside Mudita Center.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"In the sub-section showing your MuditaOS version, you will be notified whether your phone needs updating with an “Update is available” prompt.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click the “Download Now” button and follow the instructions.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"After a successful update, your Pure will automatically restart. After that, you will be able to see your MuditaOS version in the “Overview” section.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Note: Remember to not use nor unplug your Pure from the computer during the update process","marks":[{"type":"bold"}],"data":{}},{"nodeType":"text","value":". ","marks":[],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"GSBqsntyLST2hANzSdMh1":{"id":"GSBqsntyLST2hANzSdMh1","question":"How to connect my Mudita Harmony to Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Open the Mudita Center app.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Harmony to your desktop computer with a dedicated USB cable and make sure that Mudita Center recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"1OzihaTKRPEfPk3NPyRhku":{"id":"1OzihaTKRPEfPk3NPyRhku","question":"How to delete a message inside Mudita Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You currently cannot directly delete messages in the Mudita Center.\nPlease locate the desired message on your Mudita Pure and remove it from there.\nNaturally, once connected, Mudita Center will reflect the changes.","marks":[],"data":{}}],"data":{}}]}},"4kQ3BfvtPxoM1ErBSoXlMW":{"id":"4kQ3BfvtPxoM1ErBSoXlMW","question":"How to import my Google contacts to Mudita Pure?","answer":{"data":{},"content":[{"data":{},"content":[{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Go to the “Contacts” section inside the “YOUR PURE” section.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Click on “Import”.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"You will see several options to import contacts.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Select the “Continue with Google” method, it will bring you to the next modal with authorization (it means you need to log in to your Google account). ","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"Now you can choose which contacts you want to import and follow the instructions.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"},{"data":{},"content":[{"data":{},"content":[{"data":{},"marks":[],"value":"After the process is finished successfully you will be able to view the contacts in Mudita Center and on Mudita Pure.","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"list-item"}],"nodeType":"ordered-list"},{"data":{},"content":[{"data":{},"marks":[],"value":"","nodeType":"text"}],"nodeType":"paragraph"}],"nodeType":"document"}},"2W0Yh5enrN1SqWGkvGkvf2":{"id":"2W0Yh5enrN1SqWGkvGkvf2","question":"I can’t see the section YOUR PURE, how can I open it?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Make sure that Mudita Center is running and it recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Check the categories on the left bar; below “Mudita News” you will be able to see a few categories under the “YOUR PURE” section.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"3UzcSgFRo8CjfMUTycu3KB":{"id":"3UzcSgFRo8CjfMUTycu3KB","question":"How to connect my Pure to Mudita Center?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Open the Mudita Center app.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Connect your Mudita Pure to your desktop computer with a dedicated USB cable and make sure that Mudita Center recognizes your device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"You’re all set.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"5D42R98KntZbQUTcrhizxs":{"id":"5D42R98KntZbQUTcrhizxs","question":"How to import my Mudita Pure from the .vcf file?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Go to the “Contacts” section inside the “YOUR PURE” section.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click on the “Import” button.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click “Import from VCF file”, it will direct you to the system modal which allows you to choose a file from your computer. ","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Once the file is chosen to import, you will be able to see the list of the contacts (you can choose contacts to import).","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Click the confirmation button to start the import process.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"After the process is finished successfully you will be able to view the contacts in Mudita Center and on Mudita Pure.","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"4Kj1OMtlPJSTs27TkS9ELG":{"id":"4Kj1OMtlPJSTs27TkS9ELG","question":"What to do when Mudita Center does not recognise my device?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"ordered-list","content":[{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Remove the USB cable from your Mudita device and your computer.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Inspect the USB port and cable for damages or dirt.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Restart your Mudita device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Make sure you’re using the cable that came with your Mudita device.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Make sure the cable plug goes all the way into the port.","marks":[],"data":{}}],"data":{}}],"data":{}},{"nodeType":"list-item","content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Unlock your Mudita device's screen (if applicable).","marks":[],"data":{}}],"data":{}}],"data":{}}],"data":{}},{"nodeType":"paragraph","content":[{"nodeType":"text","value":"","marks":[],"data":{}}],"data":{}}]}},"1oCmuEnSQyYHsSvew1g0vm":{"id":"1oCmuEnSQyYHsSvew1g0vm","question":"How to contact support?","answer":{"nodeType":"document","data":{},"content":[{"nodeType":"paragraph","content":[{"nodeType":"text","value":"Please reach out to ","marks":[],"data":{}},{"nodeType":"hyperlink","content":[{"nodeType":"text","value":"support@mudita.com","marks":[{"type":"underline"}],"data":{}}],"data":{"uri":"mailto:support@mudita.com"}},{"nodeType":"text","value":" for assistance.","marks":[],"data":{}}],"data":{}}]}}},"nextSyncToken":"w7Ese3kdwpMbMhhgw7QAUsKiw6bCiw_ClnfDrRLCuFDDkMO5YcKMJQ_CvcKUecOnMHjCjsOGw5HDu1Fdwrs7E8KjYGgHwqPCscOJw6EYKQ7Cvw9XMAYXEXwzEsO7w6jDscKMw47ChMKIYcKnVjfCgG7CuB4eJDzCoDs7w4_DhcKmwqpuCSnCnCRIcFRYTT4rw4ZuwpNIBDt9fcOvFA"} diff --git a/packages/app/src/index-storage/observers/index-storage-loading.observer.ts b/packages/app/src/index-storage/observers/index-storage-loading.observer.ts index ac2656be1a..9ae515c0a5 100644 --- a/packages/app/src/index-storage/observers/index-storage-loading.observer.ts +++ b/packages/app/src/index-storage/observers/index-storage-loading.observer.ts @@ -14,6 +14,7 @@ import { MetadataKey } from "App/metadata/constants" import { IndexStorageService } from "App/index-storage/services" import { IpcEvent } from "App/data-sync/constants" import { DeviceManager } from "App/device-manager/services" +import { DeviceType } from "App/device" export class IndexStorageLoadingObserver implements Observer { private invoked = false @@ -42,7 +43,6 @@ export class IndexStorageLoadingObserver implements Observer { method: Method.Get, }) - // const { data } = await getDeviceInfoRequest(this.deviceService) if (!result.ok) { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/await-thenable @@ -56,7 +56,12 @@ export class IndexStorageLoadingObserver implements Observer { ) this.keyStorage.setValue(MetadataKey.DeviceToken, result.data.deviceToken) - const restored = await this.indexStorageService.loadIndex() + //CP-1668 - when connecting Kompact there is no need to read indexes :) + const restored = + this.deviceManager.currentDevice?.deviceType === + DeviceType.MuditaKompakt + ? true + : await this.indexStorageService.loadIndex() if (restored) { // AUTO DISABLED - fix me if you like :) diff --git a/packages/app/src/messages/helpers/threads.helpers.ts b/packages/app/src/messages/helpers/threads.helpers.ts index 5d7890c9d3..000fe835a6 100644 --- a/packages/app/src/messages/helpers/threads.helpers.ts +++ b/packages/app/src/messages/helpers/threads.helpers.ts @@ -69,7 +69,7 @@ export const sortMessages = (messages: Message[]): Message[] => { export const mapThreadsToReceivers = (threads: Thread[]): Receiver[] => { return threads.map(({ phoneNumber }) => ({ - phoneNumber, + phoneNumber: phoneNumber.replace(/[\s]/g, ""), identification: ReceiverIdentification.unknown, })) } @@ -96,6 +96,17 @@ export const isPhoneNumberValid = (phoneNumber: string): boolean => { export const mapContactsToReceivers = (contacts: Contact[]): Receiver[] => { return contacts .filter(isContactWithAnyNumber) + .map((contact) => { + return { + ...contact, + primaryPhoneNumber: contact.primaryPhoneNumber + ? contact.primaryPhoneNumber.replace(/[\s]/g, "") + : "", + secondaryPhoneNumber: contact.secondaryPhoneNumber + ? contact.secondaryPhoneNumber.replace(/[\s]/g, "") + : "", + } + }) .map( ({ primaryPhoneNumber = "", diff --git a/packages/app/src/messages/messages.container.tsx b/packages/app/src/messages/messages.container.tsx index d2a60b1527..30ccb11437 100644 --- a/packages/app/src/messages/messages.container.tsx +++ b/packages/app/src/messages/messages.container.tsx @@ -49,6 +49,7 @@ import { CreateMessageDataResponse } from "App/messages/services" import { PayloadAction } from "@reduxjs/toolkit" import { search, searchPreview } from "App/search/actions" import { SearchParams } from "App/search/dto" +import { templatesListSelector } from "App/templates/selectors" const mapStateToProps = (state: RootState & ReduxRootState) => ({ error: state.messages.error, @@ -70,7 +71,7 @@ const mapStateToProps = (state: RootState & ReduxRootState) => ({ NotificationResourceType.Message, NotificationMethod.Layout )(state), - templates: state.templates.data, + templates: templatesListSelector(state), selectedItems: state.messages.selectedItems, searchResult: state.messages.data.searchResult, searchPreviewResult: state.messages.data.searchPreviewResult, diff --git a/packages/app/src/messages/presenters/message.presenter.ts b/packages/app/src/messages/presenters/message.presenter.ts index 6b549d206d..85a1bc5b96 100644 --- a/packages/app/src/messages/presenters/message.presenter.ts +++ b/packages/app/src/messages/presenters/message.presenter.ts @@ -56,8 +56,9 @@ export class MessagePresenter { ): Message { const { messageBody, messageID, messageType, createdAt, threadID, number } = pureMessage + return { - phoneNumber: number, + phoneNumber: number?.replace(/[\s]/g, ""), id: String(messageID), date: new Date(createdAt * 1000), content: messageBody, diff --git a/packages/app/src/messages/presenters/thread.presenter.ts b/packages/app/src/messages/presenters/thread.presenter.ts index 40bcc84479..cbc44747e4 100644 --- a/packages/app/src/messages/presenters/thread.presenter.ts +++ b/packages/app/src/messages/presenters/thread.presenter.ts @@ -17,11 +17,12 @@ export class ThreadPresenter { number = "", messageType, } = pureThread + return { messageSnippet: ThreadPresenter.buildMessageSnippet(pureThread), unread: isUnread, id: String(threadID), - phoneNumber: String(number), + phoneNumber: String(number)?.replace(/[\s]/g, ""), lastUpdatedAt: new Date(lastUpdatedAt * 1000), messageType: ThreadPresenter.getMessageType(Number(messageType)), contactId: undefined, diff --git a/packages/app/src/messages/reducers/messages.reducer.ts b/packages/app/src/messages/reducers/messages.reducer.ts index ed90de6c90..d736095da6 100644 --- a/packages/app/src/messages/reducers/messages.reducer.ts +++ b/packages/app/src/messages/reducers/messages.reducer.ts @@ -383,8 +383,14 @@ export const messagesReducer = createReducer( .addCase( fulfilledAction(DataSyncEvent.ReadAllIndexes), (state, action: ReadAllIndexesAction) => { + const selectedItems = { + rows: state.selectedItems.rows.filter((row) => + Object.keys(action.payload.messages).includes(row) + ), + } return { ...state, + selectedItems, data: { ...state.data, threadMap: action.payload.threads, diff --git a/packages/app/src/messages/selectors/get-receiver.selector.test.ts b/packages/app/src/messages/selectors/get-receiver.selector.test.ts index cfb0060f75..5c5e6087ca 100644 --- a/packages/app/src/messages/selectors/get-receiver.selector.test.ts +++ b/packages/app/src/messages/selectors/get-receiver.selector.test.ts @@ -53,7 +53,7 @@ describe("`getReceiverSelector` selector", () => { } as RootState & ReduxRootState expect(getReceiverSelector("+48 755 853 216")(state)).toEqual({ identification: 0, - phoneNumber: "+48 755 853 216", + phoneNumber: "+48755853216", }) }) }) diff --git a/packages/app/src/messages/selectors/get-receivers.selector.test.ts b/packages/app/src/messages/selectors/get-receivers.selector.test.ts index 8001843c05..3ab593e936 100644 --- a/packages/app/src/messages/selectors/get-receivers.selector.test.ts +++ b/packages/app/src/messages/selectors/get-receivers.selector.test.ts @@ -55,7 +55,7 @@ describe("`getReceiversSelector` selector", () => { expect(getReceiversSelector(state)).toEqual([ { identification: 0, - phoneNumber: "+48 755 853 216", + phoneNumber: "+48755853216", }, ]) }) @@ -117,7 +117,7 @@ describe("`getReceiversSelector` selector", () => { firstName: "Sławomir", identification: 0, lastName: "Borewicz", - phoneNumber: "+48 755 853 216", + phoneNumber: "+48755853216", }, ]) }) @@ -148,7 +148,7 @@ describe("`getReceiversSelector` selector", () => { firstName: "Oswald", identification: 0, lastName: "Bednar", - phoneNumber: "+48 755 853 216", + phoneNumber: "+48755853216", }, ]) }) diff --git a/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.test.ts b/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.test.ts index d31d602694..f4b0f1371e 100644 --- a/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.test.ts +++ b/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.test.ts @@ -120,37 +120,6 @@ describe("async `checkAppUpdateFlowToShow` ", () => { }) }) - describe("when `updateAvailable` and `loaded` in settings is set to true but `collectingDataModalShow` is set to true", () => { - const storeState: Partial = { - settings: { - ...settingsInitialState, - updateAvailable: true, - loaded: true, - }, - modalsManager: { - ...modalsManagerInitialState, - collectingDataModalShow: true, - }, - } - - test("fire async `checkAppUpdateFlowToShow` no made any side effects", async () => { - const store = createMockStore([thunk])(storeState) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await store.dispatch( - checkAppUpdateFlowToShow() as unknown as AnyAction - ) - - expect(store.getActions()).toEqual([ - checkAppUpdateFlowToShow.pending(requestId, undefined), - checkAppUpdateFlowToShow.fulfilled(undefined, requestId, undefined), - ]) - }) - }) - describe("when `updateAvailable` and `loaded` in settings is set to true but `appForcedUpdateFlowShow` is set to true", () => { const storeState: Partial = { settings: { diff --git a/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.ts b/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.ts index 995840f6c1..7dd3704195 100644 --- a/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.ts +++ b/packages/app/src/modals-manager/actions/check-app-update-flow-to-show.action.ts @@ -18,7 +18,6 @@ export const checkAppUpdateFlowToShow = createAsyncThunk( if ( state.settings.loaded && state.settings.updateAvailable && - !state.modalsManager.collectingDataModalShow && !state.modalsManager.appForcedUpdateFlowShow ) { dispatch(showModal(ModalStateKey.AppUpdateFlow)) diff --git a/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.test.ts b/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.test.ts deleted file mode 100644 index 95a371891f..0000000000 --- a/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import createMockStore from "redux-mock-store" -import thunk from "redux-thunk" -import { AnyAction } from "@reduxjs/toolkit" -import { initialState } from "App/settings/reducers/settings.reducer" -import { ReduxRootState, RootState } from "App/__deprecated__/renderer/store" -import { ModalsManagerEvent } from "App/modals-manager/constants" -import { ModalStateKey } from "App/modals-manager/reducers" -import { checkCollectingDataModalToShow } from "App/modals-manager/actions/check-collecting-data-modal-to-show.action" - -describe("async `checkCollectingDataModalToShow` ", () => { - describe("when store state is set to default", () => { - const storeState: Partial = { - settings: initialState, - } - - test("fire async `checkCollectingDataModalToShow` no made any side effects", async () => { - const store = createMockStore([thunk])(storeState) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await store.dispatch( - checkCollectingDataModalToShow() as unknown as AnyAction - ) - - expect(store.getActions()).toEqual([ - checkCollectingDataModalToShow.pending(requestId, undefined), - checkCollectingDataModalToShow.fulfilled( - undefined, - requestId, - undefined - ), - ]) - }) - }) - - describe("when `collectingData` in settings is set to true", () => { - const storeState: Partial = { - settings: { ...initialState, collectingData: true }, - } - - test("fire async `checkCollectingDataModalToShow` no made any side effects", async () => { - const store = createMockStore([thunk])(storeState) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await store.dispatch( - checkCollectingDataModalToShow() as unknown as AnyAction - ) - - expect(store.getActions()).toEqual([ - checkCollectingDataModalToShow.pending(requestId, undefined), - checkCollectingDataModalToShow.fulfilled( - undefined, - requestId, - undefined - ), - ]) - }) - }) - - describe("when `collectingData` in settings is set to false", () => { - const storeState: Partial = { - settings: { ...initialState, collectingData: false }, - } - - test("fire async `checkCollectingDataModalToShow` no made any side effects", async () => { - const store = createMockStore([thunk])(storeState) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await store.dispatch( - checkCollectingDataModalToShow() as unknown as AnyAction - ) - - expect(store.getActions()).toEqual([ - checkCollectingDataModalToShow.pending(requestId, undefined), - checkCollectingDataModalToShow.fulfilled( - undefined, - requestId, - undefined - ), - ]) - }) - }) - - describe("when `collectingData` is set to `undefined` and `loaded` is set to true", () => { - const storeState: Partial = { - settings: { - ...initialState, - collectingData: undefined, - loaded: true, - }, - } - - test("fire async `checkCollectingDataModalToShow` call `ShowModal`", async () => { - const store = createMockStore([thunk])(storeState) - - const { - meta: { requestId }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/await-thenable - } = await store.dispatch( - checkCollectingDataModalToShow() as unknown as AnyAction - ) - - expect(store.getActions()).toEqual([ - checkCollectingDataModalToShow.pending(requestId, undefined), - { - payload: ModalStateKey.CollectingDataModal, - type: ModalsManagerEvent.ShowModal, - }, - checkCollectingDataModalToShow.fulfilled( - undefined, - requestId, - undefined - ), - ]) - }) - }) -}) diff --git a/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.ts b/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.ts deleted file mode 100644 index 43a6916a3c..0000000000 --- a/packages/app/src/modals-manager/actions/check-collecting-data-modal-to-show.action.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { createAsyncThunk } from "@reduxjs/toolkit" -import { ReduxRootState, RootState } from "App/__deprecated__/renderer/store" -import { ModalsManagerEvent } from "App/modals-manager/constants" -import { showModal } from "App/modals-manager/actions/base.action" -import { ModalStateKey } from "App/modals-manager/reducers" - -export const checkCollectingDataModalToShow = createAsyncThunk( - ModalsManagerEvent.CheckCollectingDataModalToShow, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/require-await - async (_, { getState, dispatch }) => { - const state = getState() as RootState & ReduxRootState - if (state.settings.loaded && state.settings.collectingData === undefined) { - dispatch(showModal(ModalStateKey.CollectingDataModal)) - } - } -) diff --git a/packages/app/src/modals-manager/actions/index.ts b/packages/app/src/modals-manager/actions/index.ts index f443b9041f..7903b80048 100644 --- a/packages/app/src/modals-manager/actions/index.ts +++ b/packages/app/src/modals-manager/actions/index.ts @@ -6,5 +6,4 @@ export * from "./base.action" export * from "./check-app-forced-update-flow-to-show.action" export * from "./check-app-update-flow-to-show.action" -export * from "./check-collecting-data-modal-to-show.action" export * from "./hide-collecting-data-modal.action" diff --git a/packages/app/src/modals-manager/components/modals-manager.component.test.tsx b/packages/app/src/modals-manager/components/modals-manager.component.test.tsx index 2f329c384b..451d569ff8 100644 --- a/packages/app/src/modals-manager/components/modals-manager.component.test.tsx +++ b/packages/app/src/modals-manager/components/modals-manager.component.test.tsx @@ -5,14 +5,17 @@ import React, { ComponentProps } from "react" import { Provider } from "react-redux" -import store from "App/__deprecated__/renderer/store" +import { ReduxRootState } from "App/__deprecated__/renderer/store" import { renderWithThemeAndIntl } from "App/__deprecated__/renderer/utils/render-with-theme-and-intl" import ModalsManager from "App/modals-manager/components/modals-manager.component" -import { CollectingDataModalTestIds } from "App/settings/components/collecting-data-modal/collecting-data-modal-test-ids.enum" import { AppForcedUpdateFlowTestIds } from "App/settings/components/app-forced-update-flow/app-forced-update-flow-test-ids.enum" import { AppUpdateFlowTestIds } from "App/settings/components/app-update-flow/app-update-flow-test-ids.enum" import { ContactSupportFlowTestIds } from "App/contact-support/components/contact-support-flow-test-ids.component" import { ErrorConnectingModalTestIds } from "App/connecting/components/error-connecting-modal-test-ids.enum" +import createMockStore from "redux-mock-store" +import thunk from "redux-thunk" +import { initialState as updateInitialState } from "App/update/reducers/update-os.reducer" +import { initialState as contactSupportInitialState } from "App/contact-support/reducers/contact-support.reducer" jest.mock( "App/modals-manager/selectors/device-initialization-failed-modal-show-enabled.selector" @@ -37,21 +40,39 @@ jest.mock( type Props = ComponentProps const defaultProps: Props = { - collectingDataModalShow: false, appForcedUpdateFlowShow: false, appUpdateFlowShow: false, contactSupportFlowShow: false, - deviceInitializationFailedModalShowEnabled: false, hideModals: jest.fn(), } -const render = (extraProps?: Partial) => { +const defaultState = { + update: { + ...updateInitialState, + }, + contactSupport: { + ...contactSupportInitialState, + }, + settings: { + privacyPolicyAccepted: true, + }, +} as unknown as ReduxRootState + +const render = ( + extraProps?: Partial, + extraState?: Partial +) => { + const storeMock = createMockStore([thunk])({ + ...defaultState, + ...extraState, + }) + const props = { ...defaultProps, ...extraProps, } return renderWithThemeAndIntl( - + ) @@ -62,31 +83,6 @@ describe("`ModalsManager` component", () => { test("no modal is visible", () => { const { queryByTestId } = render() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(AppForcedUpdateFlowTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(AppUpdateFlowTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(ContactSupportFlowTestIds.ContactSupportModal) - ).not.toBeInTheDocument() - expect( - queryByTestId(ErrorConnectingModalTestIds.Container) - ).not.toBeInTheDocument() - }) - }) - - describe("when component is render with proper where `collectingDataModalShow` is set to `true`", () => { - test("`CollectingDataModal` is visible", () => { - const { queryByTestId } = render({ collectingDataModalShow: true }) - - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).toBeInTheDocument() expect( queryByTestId(AppForcedUpdateFlowTestIds.Container) ).not.toBeInTheDocument() @@ -109,9 +105,6 @@ describe("`ModalsManager` component", () => { expect( queryByTestId(AppForcedUpdateFlowTestIds.Container) ).toBeInTheDocument() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() expect( queryByTestId(AppUpdateFlowTestIds.Container) ).not.toBeInTheDocument() @@ -132,9 +125,6 @@ describe("`ModalsManager` component", () => { expect( queryByTestId(AppForcedUpdateFlowTestIds.Container) ).not.toBeInTheDocument() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() expect( queryByTestId(ContactSupportFlowTestIds.ContactSupportModal) ).not.toBeInTheDocument() @@ -157,35 +147,8 @@ describe("`ModalsManager` component", () => { expect( queryByTestId(AppForcedUpdateFlowTestIds.Container) ).not.toBeInTheDocument() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(ErrorConnectingModalTestIds.Container) - ).not.toBeInTheDocument() - }) - }) - - describe("when component is render with proper where `deviceInitializationFailedModalShowEnabled` is set to `true`", () => { - test("`ErrorConnectingModal` is visible", () => { - const { queryByTestId } = render({ - deviceInitializationFailedModalShowEnabled: true, - }) - expect( queryByTestId(ErrorConnectingModalTestIds.Container) - ).toBeInTheDocument() - expect( - queryByTestId(ContactSupportFlowTestIds.ContactSupportModal) - ).not.toBeInTheDocument() - expect( - queryByTestId(AppUpdateFlowTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(AppForcedUpdateFlowTestIds.Container) - ).not.toBeInTheDocument() - expect( - queryByTestId(CollectingDataModalTestIds.Container) ).not.toBeInTheDocument() }) }) diff --git a/packages/app/src/modals-manager/components/modals-manager.component.tsx b/packages/app/src/modals-manager/components/modals-manager.component.tsx index bb13043a0f..1f5ae1c5e5 100644 --- a/packages/app/src/modals-manager/components/modals-manager.component.tsx +++ b/packages/app/src/modals-manager/components/modals-manager.component.tsx @@ -6,37 +6,37 @@ import React from "react" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import { - CollectingDataModalContainer, AppForcedUpdateFlowContainer, AppUpdateFlowContainer, } from "App/settings/components" import ContactSupportFlow from "App/contact-support/containers/contact-support-flow.container" import { UpdateOsInterruptedFlowContainer } from "App/update/components/update-os-interrupted-flow" -import ErrorConnectingModal from "App/connecting/components/error-connecting-modal" +import { useSelector } from "react-redux" +import { ReduxRootState } from "App/__deprecated__/renderer/store" +import PrivacyPolicyModal from "App/settings/components/privacy-policy-modal/privacy-policy-modal.component" type Props = { - collectingDataModalShow: boolean appForcedUpdateFlowShow: boolean appUpdateFlowShow: boolean contactSupportFlowShow: boolean - deviceInitializationFailedModalShowEnabled: boolean hideModals: () => void } const ModalsManager: FunctionComponent = ({ - collectingDataModalShow, appForcedUpdateFlowShow, appUpdateFlowShow, contactSupportFlowShow, - deviceInitializationFailedModalShowEnabled, - hideModals, }) => { + const { privacyPolicyAccepted } = useSelector( + (state: ReduxRootState) => state.settings + ) + + if (privacyPolicyAccepted === false) { + return + } + return ( <> - {deviceInitializationFailedModalShowEnabled && ( - - )} - {appForcedUpdateFlowShow && } {appUpdateFlowShow && } {contactSupportFlowShow && } diff --git a/packages/app/src/modals-manager/constants/modal-layers.enum.ts b/packages/app/src/modals-manager/constants/modal-layers.enum.ts new file mode 100644 index 0000000000..ac09f0a424 --- /dev/null +++ b/packages/app/src/modals-manager/constants/modal-layers.enum.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +export enum ModalLayers { + Default = 5, + CrashDump, + UpdateOS, + ContactSupport, + Passcode, + EULA, + ErrorConnecting, + UpdateApp, + PrivacyPolicy, +} diff --git a/packages/app/src/modals-manager/containers/modals-manager.container.test.tsx b/packages/app/src/modals-manager/containers/modals-manager.container.test.tsx index 3ddcad0514..d1e66e1552 100644 --- a/packages/app/src/modals-manager/containers/modals-manager.container.test.tsx +++ b/packages/app/src/modals-manager/containers/modals-manager.container.test.tsx @@ -8,7 +8,6 @@ import { Provider } from "react-redux" import store from "App/__deprecated__/renderer/store" import { renderWithThemeAndIntl } from "App/__deprecated__/renderer/utils/render-with-theme-and-intl" import ModalsManager from "App/modals-manager/containers/modals-manager.container" -import { CollectingDataModalTestIds } from "App/settings/components/collecting-data-modal/collecting-data-modal-test-ids.enum" import { AppForcedUpdateFlowTestIds } from "App/settings/components/app-forced-update-flow/app-forced-update-flow-test-ids.enum" import { AppUpdateFlowTestIds } from "App/settings/components/app-update-flow/app-update-flow-test-ids.enum" @@ -44,9 +43,6 @@ describe("`ModalsManager` container", () => { test("no modal is visible", () => { const { queryByTestId } = render() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() expect( queryByTestId(AppForcedUpdateFlowTestIds.Container) ).not.toBeInTheDocument() diff --git a/packages/app/src/modals-manager/reducers/modals-manager.interface.ts b/packages/app/src/modals-manager/reducers/modals-manager.interface.ts index 0699a5a814..1155b6e22a 100644 --- a/packages/app/src/modals-manager/reducers/modals-manager.interface.ts +++ b/packages/app/src/modals-manager/reducers/modals-manager.interface.ts @@ -7,7 +7,6 @@ import { PayloadAction } from "@reduxjs/toolkit" import { ModalsManagerEvent } from "App/modals-manager/constants" export enum ModalStateKey { - CollectingDataModal = "collectingDataModalShow", AppForcedUpdateFlow = "appForcedUpdateFlowShow", AppUpdateFlow = "appUpdateFlowShow", ContactSupportFlow = "contactSupportFlowShow", @@ -15,7 +14,6 @@ export enum ModalStateKey { } export interface ModalsManagerState extends Record { - collectingDataModalShow: boolean appForcedUpdateFlowShow: boolean appUpdateFlowShow: boolean contactSupportFlowShow: boolean diff --git a/packages/app/src/modals-manager/reducers/modals-manager.reducer.ts b/packages/app/src/modals-manager/reducers/modals-manager.reducer.ts index 4f08279b5c..4e65a889ee 100644 --- a/packages/app/src/modals-manager/reducers/modals-manager.reducer.ts +++ b/packages/app/src/modals-manager/reducers/modals-manager.reducer.ts @@ -12,7 +12,6 @@ import { import { ModalsManagerEvent } from "App/modals-manager/constants" const initialModalsState: Record = { - collectingDataModalShow: false, appForcedUpdateFlowShow: false, appUpdateFlowShow: false, contactSupportFlowShow: false, diff --git a/packages/app/src/modals-manager/selectors/device-initialization-failed-modal-show-enabled.selector.ts b/packages/app/src/modals-manager/selectors/device-initialization-failed-modal-show-enabled.selector.ts index 0ee026b5fb..c6a9e3e96e 100644 --- a/packages/app/src/modals-manager/selectors/device-initialization-failed-modal-show-enabled.selector.ts +++ b/packages/app/src/modals-manager/selectors/device-initialization-failed-modal-show-enabled.selector.ts @@ -43,7 +43,6 @@ export const deviceInitializationFailedModalShowEnabledSelector = settingsState.loaded && deviceState.state === ConnectionState.Error && // TODO: Move manage an order of the modal displaying to the view component - !modalsManagerState.collectingDataModalShow && !modalsManagerState.appForcedUpdateFlowShow && !modalsManagerState.appUpdateFlowShow && !modalsManagerState.contactSupportFlowShow && diff --git a/packages/app/src/modals-manager/selectors/no-modals-show.selector.test.ts b/packages/app/src/modals-manager/selectors/no-modals-show.selector.test.ts index 1efc7fa868..f1c089f1ba 100644 --- a/packages/app/src/modals-manager/selectors/no-modals-show.selector.test.ts +++ b/packages/app/src/modals-manager/selectors/no-modals-show.selector.test.ts @@ -17,18 +17,6 @@ describe("`noModalsShowSelector` selector", () => { expect(noModalsShowSelector(state)).toBeTruthy() }) - test("when `collectingDataModalShow` property is set to true", () => { - const state = { - modalsManager: modalsManagerReducer( - { ...initialState, collectingDataModalShow: true }, - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - {} as any - ), - } as ReduxRootState - expect(noModalsShowSelector(state)).toBeFalsy() - }) - test("when `appForcedUpdateFlowShow` property is set to true", () => { const state = { modalsManager: modalsManagerReducer( @@ -60,7 +48,6 @@ describe("`noModalsShowSelector` selector", () => { ...initialState, appUpdateFlowShow: true, appForcedUpdateFlowShow: true, - collectingDataModalShow: true, }, // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/app/src/overview/components/backup-modal-dialogs/backup-modal-dialogs.tsx b/packages/app/src/overview/components/backup-modal-dialogs/backup-modal-dialogs.tsx index 4ed39104ae..f5559c9fd7 100644 --- a/packages/app/src/overview/components/backup-modal-dialogs/backup-modal-dialogs.tsx +++ b/packages/app/src/overview/components/backup-modal-dialogs/backup-modal-dialogs.tsx @@ -59,6 +59,9 @@ const messages = defineMessages({ backupNoSpaceFailureModalDescription: { id: "module.overview.backupNoSpaceFailureModalDescription", }, + backupNoSpaceFailureWithoutDetailsModalDescription: { + id: "module.overview.backupNoSpaceFailureWithoutDetailsModalDescription", + }, backupFailureModalSecondaryButton: { id: "module.overview.backupFailureModalSecondaryButton", }, @@ -207,6 +210,15 @@ interface NoSpaceBackupFailureModalProps export const NoSpaceBackupFailureModal: FunctionComponent< NoSpaceBackupFailureModalProps > = ({ size, secondaryActionButtonClick, onClose, ...props }) => { + const sizeInBytes = convertBytes(size) + + const errorMessage = isNaN(size) + ? messages.backupNoSpaceFailureWithoutDetailsModalDescription + : { + ...messages.backupNoSpaceFailureModalDescription, + values: { size: sizeInBytes }, + } + const handleOnClose = (): void => { if (onClose) { onClose() @@ -232,10 +244,7 @@ export const NoSpaceBackupFailureModal: FunctionComponent< ) diff --git a/packages/app/src/overview/components/backup-set-secret-key-modal-dialog/backup-set-secret-key-modal-dialog.component.tsx b/packages/app/src/overview/components/backup-set-secret-key-modal-dialog/backup-set-secret-key-modal-dialog.component.tsx index 74835e221e..b5b87cffc0 100644 --- a/packages/app/src/overview/components/backup-set-secret-key-modal-dialog/backup-set-secret-key-modal-dialog.component.tsx +++ b/packages/app/src/overview/components/backup-set-secret-key-modal-dialog/backup-set-secret-key-modal-dialog.component.tsx @@ -4,7 +4,7 @@ */ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import React, { ComponentProps } from "react" +import React, { ComponentProps, useEffect } from "react" import { ModalDialog } from "App/ui/components/modal-dialog" import { intl } from "App/__deprecated__/renderer/utils/intl" import { ModalText } from "App/contacts/components/sync-contacts-modal/sync-contacts.styled" @@ -118,15 +118,22 @@ export const BackupSetSecretKeyModal: FunctionComponent< watch, handleSubmit, formState: { errors }, + trigger, } = useForm({ mode: "onChange", }) - const fields = watch() + const password = watch(FieldKeys.SecretKey) const handleSubmitClick = handleSubmit((data) => { onSecretKeySet(data.secretKey) }) + useEffect(() => { + if (password) { + void trigger(FieldKeys.ConfirmationSecretKey) + } + }, [password, trigger]) + return ( { - if (value === fields[FieldKeys.SecretKey]) { + if (value === password) { return } diff --git a/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.component.tsx b/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.component.tsx index a42575a76d..2bc9f37123 100644 --- a/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.component.tsx +++ b/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.component.tsx @@ -35,7 +35,7 @@ const messages = defineMessages({ export const OnboardingNotCompleteModal: FunctionComponent< OnboardingNotCompleteModalProps -> = ({ onClose, open, testId }) => { +> = ({ onClose, open, testId, ...rest }) => { return ( diff --git a/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.interface.ts b/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.interface.ts index e0e8ad3d0d..f769dd7e1b 100644 --- a/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.interface.ts +++ b/packages/app/src/overview/components/onboarding-not-complete-modal/onboarding-not-complete-modal.interface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface OnboardingNotCompleteModalProps { +import { ModalDialogProps } from "App/ui" + +export interface OnboardingNotCompleteModalProps extends ModalDialogProps { open: boolean onClose: () => void testId?: string diff --git a/packages/app/src/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx b/packages/app/src/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx index da263c6d78..4a36000f51 100644 --- a/packages/app/src/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx +++ b/packages/app/src/overview/components/overview-screens/harmony-overview/harmony-overview.component.tsx @@ -98,27 +98,29 @@ export const HarmonyOverview: FunctionComponent = ({ return ( <> - + {!forceUpdateNeeded && ( + + )} {flags.get(Feature.ForceUpdate) && ( | null + readonly syncState: SynchronizationState + readonly serialNumber: string | undefined + readonly silentCheckForUpdateState: SilentCheckForUpdateState + readonly checkingForUpdateState: CheckForUpdateState + readonly availableReleasesForUpdate: OsRelease[] | null + readonly downloadingState: DownloadState + readonly allReleases: OsRelease[] | null + readonly updateOsError: AppError | null + readonly downloadingReleasesProcessStates: ProcessedRelease[] | null + readonly updatingReleasesProcessStates: ProcessedRelease[] | null + readonly areAllReleasesDownloaded: boolean + readonly forceUpdateNeeded: boolean + readonly updateAllIndexes: () => Promise + readonly openContactSupportFlow: () => void + readonly readRestoreDeviceDataState: () => void + readonly startRestoreDevice: (option: RestoreBackup) => void + readonly readBackupDeviceDataState: () => void + readonly startBackupDevice: (secretKey: string) => void + readonly closeForceUpdateFlow: () => void + readonly startUpdateOs: (releases: OsRelease[]) => void + readonly disconnectDevice: () => void + readonly checkForUpdate: ( + deviceType: DeviceType, + mode: CheckForUpdateMode + ) => RejectableThunk + readonly setCheckForUpdateState: (state: CheckForUpdateState) => void + readonly downloadUpdates: (releases: OsRelease[]) => void + readonly clearUpdateState: () => void + readonly abortDownload: () => void + forceUpdate: (releases: OsRelease[]) => void + readonly backupActionDisabled: boolean +} diff --git a/packages/app/src/overview/components/overview-screens/kompakt-overview/kompakt-overview.component.tsx b/packages/app/src/overview/components/overview-screens/kompakt-overview/kompakt-overview.component.tsx new file mode 100644 index 0000000000..a629bdf02a --- /dev/null +++ b/packages/app/src/overview/components/overview-screens/kompakt-overview/kompakt-overview.component.tsx @@ -0,0 +1,34 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React from "react" +import { ReduxRootState } from "App/__deprecated__/renderer/store" +import { useSelector } from "react-redux" +import { KompaktDeviceData } from "App/device/reducers" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" + +export const KompaktOverview: FunctionComponent = () => { + const { + batteryLevel, + caseColour, + networkLevel, + networkName, + serialNumber, + osVersion, + } = useSelector( + (state: ReduxRootState) => state.device.data as KompaktDeviceData + ) + + return ( +
+

batteryLevel: {batteryLevel * 100}%

+

caseColour: {caseColour}

+

networkName: {networkName}

+

networkLevel: {networkLevel}

+

serialNumber: {serialNumber}

+

osVersion: {osVersion}

+
+ ) +} diff --git a/packages/app/src/overview/components/overview/overview.component.tsx b/packages/app/src/overview/components/overview/overview.component.tsx index b4a1acbcf2..83e195c4ef 100644 --- a/packages/app/src/overview/components/overview/overview.component.tsx +++ b/packages/app/src/overview/components/overview/overview.component.tsx @@ -9,6 +9,7 @@ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-co import { PureOverview, HarmonyOverview, + KompaktOverview, } from "App/overview/components/overview-screens" type PureOverviewProps = ComponentProps @@ -22,6 +23,8 @@ const Screen: FunctionComponent = (props) => { return case DeviceType.MuditaHarmony: return + case DeviceType.MuditaKompakt: + return default: return <> } diff --git a/packages/app/src/overview/components/update-os-flow/update-os-flow.component.interface.ts b/packages/app/src/overview/components/update-os-flow/update-os-flow.component.interface.ts index 770bba8f17..2b38bbde8e 100644 --- a/packages/app/src/overview/components/update-os-flow/update-os-flow.component.interface.ts +++ b/packages/app/src/overview/components/update-os-flow/update-os-flow.component.interface.ts @@ -6,6 +6,7 @@ import { State } from "App/core/constants" import { AppError } from "App/core/errors" import { DeviceType } from "App/device" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" import { DownloadState, SilentCheckForUpdateState, @@ -34,4 +35,5 @@ export interface UpdateOsFlowProps { openContactSupportFlow: () => void openHelpView: () => void tryAgainCheckForUpdate: () => void + layer?: ModalLayers } diff --git a/packages/app/src/overview/components/update-os-flow/update-os-flow.component.tsx b/packages/app/src/overview/components/update-os-flow/update-os-flow.component.tsx index 8644105d07..aff8d3cb05 100644 --- a/packages/app/src/overview/components/update-os-flow/update-os-flow.component.tsx +++ b/packages/app/src/overview/components/update-os-flow/update-os-flow.component.tsx @@ -32,6 +32,7 @@ import React, { FunctionComponent, useMemo } from "react" import { NotEnoughSpaceModal } from "App/overview/components/update-os-modals/not-enough-space-modal" import { OnboardingNotCompleteModal } from "App/overview/components/onboarding-not-complete-modal" import { CheckForUpdateState } from "App/update/constants/check-for-update-state.constant" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" export const UpdateOsFlow: FunctionComponent = ({ checkForUpdateState, @@ -53,6 +54,7 @@ export const UpdateOsFlow: FunctionComponent = ({ tryAgainCheckForUpdate, areAllReleasesDownloaded, deviceType, + layer = ModalLayers.UpdateOS, }) => { const { devRelease, @@ -106,11 +108,13 @@ export const UpdateOsFlow: FunctionComponent = ({ return ( <> {availableReleasesForUpdate && availableReleasesForUpdate.length > 0 && ( = ({ {(!availableReleasesForUpdate || availableReleasesForUpdate.length === 0) && ( = ({ )} = ({ {downloadingReleasesProcessStates && currentlyDownloadedReleaseIndex >= 0 && ( = ({ )} = ({ onContactSupport={openContactSupportFlow} /> = ({ {updatingReleasesProcessStates && currentlyInstalledReleaseIndex >= 0 && ( = ({ )} = ({ /> = ({ /> = ({ /> = ({ {devRelease && ( = - ({ - onContactSupport, - onTryAgain, - onClose, - open, - testId, - }: CheckForUpdateFailedModalProps) => { - return ( - - ) - } +export const CheckForUpdateFailedModal: FunctionComponent< + CheckForUpdateFailedModalProps +> = ({ + onContactSupport, + onTryAgain, + onClose, + open, + testId, + ...rest +}: CheckForUpdateFailedModalProps) => { + return ( + + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/check-for-update-failed-modal/check-for-update-failed-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/check-for-update-failed-modal/check-for-update-failed-modal.interface.ts index f7b64abb98..7907c654a7 100644 --- a/packages/app/src/overview/components/update-os-modals/check-for-update-failed-modal/check-for-update-failed-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/check-for-update-failed-modal/check-for-update-failed-modal.interface.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface CheckForUpdateFailedModalProps { +import { ModalDialogProps } from "App/ui" + +export interface CheckForUpdateFailedModalProps + extends Omit { open: boolean testId?: string onContactSupport: () => void diff --git a/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.component.tsx index 30bd362e4c..7de08c3238 100644 --- a/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.component.tsx @@ -20,13 +20,14 @@ const messages = defineMessages({ export const CheckingUpdatesModal: FunctionComponent< CheckingUpdatesModalProps -> = ({ open, testId }) => { +> = ({ open, testId, ...rest }) => { return ( ) } diff --git a/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.interface.ts index 3f027c64dc..a01c206d46 100644 --- a/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/checking-updates-modal/checking-updates-modal.interface.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface CheckingUpdatesModalProps { +import { ModalDialogProps } from "App/ui" + +export interface CheckingUpdatesModalProps + extends Omit { open: boolean testId?: string } diff --git a/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.component.tsx index 53f068666c..b46f9f4fd6 100644 --- a/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.component.tsx @@ -22,6 +22,7 @@ export const DevUpdateModal: FunctionComponent = ({ open, onClose, testId, + ...rest }) => { const textDate = new Date(date).toLocaleDateString("en-US", { year: "numeric", @@ -36,6 +37,7 @@ export const DevUpdateModal: FunctionComponent = ({ actionButtonLabel={install ? "Install now" : "Download now"} onActionButtonClick={action} closeModal={onClose} + {...rest} > diff --git a/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.inteface.ts b/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.inteface.ts index 9562cf5a28..73a9d8e832 100644 --- a/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.inteface.ts +++ b/packages/app/src/overview/components/update-os-modals/dev-update-modal/dev-update-modal.inteface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface DevUpdateModalProps { +import { ModalDialogProps } from "App/ui" + +export interface DevUpdateModalProps extends ModalDialogProps { action: () => void install: boolean version: string diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.component.tsx index 09f49a56bf..8f7f4cb537 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.component.tsx @@ -29,34 +29,35 @@ const messages = defineMessages({ }, }) -export const DownloadingUpdateFailedModal: FunctionComponent = - ({ - open, - onContactSupport, - onGoToHelp, - onClose, - testId, - }: DownloadingUpdateFailedModalProps) => { - return ( - - ) - } +export const DownloadingUpdateFailedModal: FunctionComponent< + DownloadingUpdateFailedModalProps +> = ({ + open, + onContactSupport, + onGoToHelp, + onClose, + testId, + ...rest +}: DownloadingUpdateFailedModalProps) => { + return ( + + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.interface.ts index 0d1dd8a21d..ae97ae0db4 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-failed-modal/downloading-update-failed-modal.interface.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface DownloadingUpdateFailedModalProps { +import { ModalDialogProps } from "App/ui" + +export interface DownloadingUpdateFailedModalProps + extends Omit { onContactSupport: () => void onGoToHelp: () => void onClose: () => void diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.component.tsx index 8218f3b98b..d24eb6ec77 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.component.tsx @@ -32,7 +32,7 @@ const messages = defineMessages({ export const DownloadingUpdateFinishedModal: FunctionComponent< DownloadingUpdateFinishedModalProps -> = ({ onOsUpdate, open, onClose, testId, downloadedReleases }) => { +> = ({ onOsUpdate, open, onClose, testId, downloadedReleases, ...rest }) => { const handleUpdateButtonClick = (): void => { onOsUpdate() } @@ -52,6 +52,7 @@ export const DownloadingUpdateFinishedModal: FunctionComponent< closeButtonLabel={intl.formatMessage( messages.downloadCompletedCloseButton )} + {...rest} > diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.interface.ts index 116c796fdb..8cdaea5e24 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-finished-modal/downloading-update-finished-modal.interface.ts @@ -3,11 +3,13 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { ModalDialogProps } from "App/ui" + interface Release { version: string } -export interface DownloadingUpdateFinishedModalProps { +export interface DownloadingUpdateFinishedModalProps extends ModalDialogProps { onOsUpdate: () => void open: boolean onClose: () => void diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.component.tsx index 86e32a477f..172214ed2f 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.component.tsx @@ -28,41 +28,43 @@ const messages = defineMessages({ }, }) -export const DownloadingUpdateInterruptedModal: FunctionComponent = - ({ open, onClose, testId, alreadyDownloadedReleases }) => { - const formattedVersionsText = useMemo( - () => - alreadyDownloadedReleases - .map((release) => `MuditaOS v${release.version}`) - .join(" / "), - [alreadyDownloadedReleases] - ) +export const DownloadingUpdateInterruptedModal: FunctionComponent< + DownloadingUpdateInterruptedModalProps +> = ({ open, onClose, testId, alreadyDownloadedReleases, ...rest }) => { + const formattedVersionsText = useMemo( + () => + alreadyDownloadedReleases + .map((release) => `MuditaOS v${release.version}`) + .join(" / "), + [alreadyDownloadedReleases] + ) - return ( - 0 - ? { - ...messages.downloadingAbortedBody, - values: { - versionsAmount: formattedVersionsText.length, - data: formattedVersionsText, - ...textFormatters, - }, - } - : undefined - } - subbody={intl.formatMessage(messages.downloadingAbortedInstruction)} - /> - ) - } + return ( + 0 + ? { + ...messages.downloadingAbortedBody, + values: { + versionsAmount: formattedVersionsText.length, + data: formattedVersionsText, + ...textFormatters, + }, + } + : undefined + } + subbody={intl.formatMessage(messages.downloadingAbortedInstruction)} + {...rest} + /> + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.interface.ts index 86c120ea58..5d74e17f8f 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-interrupted-modal/downloading-update-interrupted-modal.interface.ts @@ -3,11 +3,14 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ +import { ModalDialogProps } from "App/ui" + interface Release { version: string } -export interface DownloadingUpdateInterruptedModalProps { +export interface DownloadingUpdateInterruptedModalProps + extends Omit { open: boolean onClose: () => void testId?: string diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.component.tsx index 3ce54620f0..5b7e5f39f9 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.component.tsx @@ -38,49 +38,52 @@ const messages = defineMessages({ }, }) -export const DownloadingUpdateModal: FunctionComponent = - ({ - percent, - open, - onCancel, - testId, - currentlyDownloadingReleaseVersion, - currentlyDownloadingReleaseOrder, - downloadedReleasesSize, - }) => { - return ( - - - - - - - - - - - {percent}% - - - - ) - } +export const DownloadingUpdateModal: FunctionComponent< + DownloadingUpdateModalProps +> = ({ + percent, + open, + onCancel, + testId, + currentlyDownloadingReleaseVersion, + currentlyDownloadingReleaseOrder, + downloadedReleasesSize, + ...rest +}) => { + return ( + + + + + + + + + + + {percent}% + + + + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.interface.ts index 43a8898ab7..233f007e3e 100644 --- a/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/downloading-update-modal/downloading-update-modal.interface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface DownloadingUpdateModalProps { +import { ModalDialogProps } from "App/ui" + +export interface DownloadingUpdateModalProps extends ModalDialogProps { open: boolean percent: number currentlyDownloadingReleaseVersion: string diff --git a/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.component.tsx index 773516fef0..6d4794dcc6 100644 --- a/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.component.tsx @@ -40,13 +40,18 @@ const messages = defineMessages({ export const ForceUpdateAvailableModal: FunctionComponent< ForceUpdateAvailableModalProps -> = ({ open, releases, onUpdate, testId }) => { +> = ({ open, releases, onUpdate, testId, layer }) => { const handleButtonClick = (_event: unknown) => { onUpdate() } return ( - + diff --git a/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.interface.ts index 1108469df2..0d695ece0b 100644 --- a/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/force-update-available-modal/force-update-available-modal.interface.ts @@ -4,8 +4,11 @@ */ import { AboutUpdatesSectionProps } from "App/overview/components/update-os-modals/update-available-modal/about-updates-section.component" +import { ModalDialog } from "App/ui" +import { ComponentProps } from "react" -export interface ForceUpdateAvailableModalProps { +export interface ForceUpdateAvailableModalProps + extends ComponentProps { onUpdate: () => void open: boolean releases: AboutUpdatesSectionProps["releases"] diff --git a/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.component.tsx index 5c8885f8b6..af20e360a6 100644 --- a/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.component.tsx @@ -34,7 +34,9 @@ const messages = defineMessages({ export const NotEnoughSpaceModal: FunctionComponent< NotEnoughSpaceModalProps -> = ({ onClose, open, testId, fileSize }) => { +> = ({ onClose, open, testId, fileSize, ...rest }) => { + const requirerSize = Math.ceil(fileSize / 1000000) * 3 + return ( @@ -57,7 +60,7 @@ export const NotEnoughSpaceModal: FunctionComponent< color="secondary" > {intl.formatMessage(messages.updatingNotEnoughSpaceDescription, { - value: Math.ceil(fileSize / 1000000), + value: requirerSize, })} diff --git a/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.interface.ts index 1215b6f044..23db4646a7 100644 --- a/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/not-enough-space-modal/not-enough-space-modal.interface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface NotEnoughSpaceModalProps { +import { ModalDialogProps } from "App/ui" + +export interface NotEnoughSpaceModalProps extends ModalDialogProps { open: boolean onClose: () => void testId?: string diff --git a/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.component.tsx index 9b46944cbd..34197d48b3 100644 --- a/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.component.tsx @@ -39,6 +39,7 @@ export const TooLowBatteryModal: FunctionComponent = ({ onClose, open, testId, + ...rest }) => { return ( = ({ closeButtonLabel={intl.formatMessage( messages.updatingFlatBatteryActionButton )} + {...rest} > diff --git a/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.interface.ts index e685ae626d..4dcf02466b 100644 --- a/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/too-low-battery-modal/too-low-battery-modal.interface.ts @@ -4,8 +4,9 @@ */ import { DeviceType } from "App/device/constants" +import { ModalDialogProps } from "App/ui" -export interface TooLowBatteryModalProps { +export interface TooLowBatteryModalProps extends ModalDialogProps { deviceType: DeviceType open: boolean onClose: () => void diff --git a/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.component.tsx index 945a720b41..f35fb13f88 100644 --- a/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.component.tsx @@ -55,6 +55,7 @@ export const UpdateAvailableModal: FunctionComponent< areAllReleasesDownloaded, onUpdate, testId, + ...rest }) => { const handleButtonClick = (_event: unknown) => { if (areAllReleasesDownloaded) { @@ -71,6 +72,7 @@ export const UpdateAvailableModal: FunctionComponent< closeable closeModal={onClose} size={ModalSize.Medium} + {...rest} > diff --git a/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.interface.ts index 8b1ef03b00..1cee4e6c7d 100644 --- a/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/update-available-modal/update-available-modal.interface.ts @@ -4,8 +4,9 @@ */ import { AboutUpdatesSectionProps } from "App/overview/components/update-os-modals/update-available-modal/about-updates-section.component" +import { ModalDialogProps } from "App/ui" -export interface UpdateAvailableModalProps { +export interface UpdateAvailableModalProps extends ModalDialogProps { onDownload: () => void onClose: () => void onUpdate: () => void diff --git a/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.component.tsx index b355d55e0f..df7e3eb14a 100644 --- a/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.component.tsx @@ -30,7 +30,7 @@ const messages = defineMessages({ export const UpdateNotAvailableModal: FunctionComponent< UpdateNotAvailableModalProps -> = ({ version, open, onClose, testId }) => ( +> = ({ version, open, onClose, testId, ...rest }) => ( diff --git a/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.interface.ts index e57a1cbe0c..47958ec80f 100644 --- a/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/update-not-available-modal/update-not-available-modal.interface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface UpdateNotAvailableModalProps { +import { ModalDialogProps } from "App/ui" + +export interface UpdateNotAvailableModalProps extends ModalDialogProps { version: string open: boolean onClose: () => void diff --git a/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.component.tsx index 5dcbd906f2..edccff3097 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.component.tsx @@ -28,33 +28,34 @@ const messages = defineMessages({ }, }) -export const UpdatingFailureWithHelpModal: FunctionComponent = - ({ - onContact, - onHelp, - onClose, - open, - testId, - }: UpdatingFailureWithHelpModalProps) => { - return ( - - ) - } +export const UpdatingFailureWithHelpModal: FunctionComponent< + UpdatingFailureWithHelpModalProps +> = ({ + onContact, + onHelp, + onClose, + open, + testId, + ...rest +}: UpdatingFailureWithHelpModalProps) => { + return ( + + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.interface.ts index 3a4dc637a5..5c0fbd7e59 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/updating-failure-with-help-modal/updating-failure-with-help-modal.interface.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface UpdatingFailureWithHelpModalProps { +import { ModalDialogProps } from "App/ui" + +export interface UpdatingFailureWithHelpModalProps + extends Omit { open: boolean onClose: () => void onContact: () => void diff --git a/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.component.tsx index 48ae3578f7..bd2f02f1bf 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.component.tsx @@ -26,25 +26,27 @@ const messages = defineMessages({ }, }) -export const UpdatingSpinnerModal: FunctionComponent = - ({ open, testId, progressParams }) => { - return ( - - {progressParams && ( - - )} - - ) - } +export const UpdatingSpinnerModal: FunctionComponent< + UpdatingSpinnerModalProps +> = ({ open, testId, progressParams, ...rest }) => { + return ( + + {progressParams && ( + + )} + + ) +} diff --git a/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.interface.ts index 87133e71c2..e4c83582c9 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/updating-spinner-modal/updating-spinner-modal.interface.ts @@ -3,7 +3,10 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface UpdatingSpinnerModalProps { +import { ModalDialogProps } from "App/ui" + +export interface UpdatingSpinnerModalProps + extends Omit { open: boolean testId?: string progressParams?: { diff --git a/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.component.tsx b/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.component.tsx index 64b45e37d1..dc813df9e5 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.component.tsx +++ b/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.component.tsx @@ -25,7 +25,7 @@ const messages = defineMessages({ export const UpdatingSuccessModal: FunctionComponent< UpdatingSuccessModalProps -> = ({ open, onClose, testId }) => ( +> = ({ open, onClose, testId, ...rest }) => ( diff --git a/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.interface.ts b/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.interface.ts index 5e017d99d2..ad0c276bf1 100644 --- a/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.interface.ts +++ b/packages/app/src/overview/components/update-os-modals/updating-success-modal/updating-success-modal.interface.ts @@ -3,7 +3,9 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export interface UpdatingSuccessModalProps { +import { ModalDialogProps } from "App/ui" + +export interface UpdatingSuccessModalProps extends ModalDialogProps { open: boolean onClose: () => void testId?: string diff --git a/packages/app/src/overview/components/updating-force-modal-flow/updating-force-modal-flow.component.tsx b/packages/app/src/overview/components/updating-force-modal-flow/updating-force-modal-flow.component.tsx index 8c899c3b35..f9535864b3 100644 --- a/packages/app/src/overview/components/updating-force-modal-flow/updating-force-modal-flow.component.tsx +++ b/packages/app/src/overview/components/updating-force-modal-flow/updating-force-modal-flow.component.tsx @@ -17,6 +17,7 @@ import { FunctionComponent } from "App/__deprecated__/renderer/types/function-co import { NotEnoughSpaceModal } from "App/overview/components/update-os-modals/not-enough-space-modal" import { UpdateOsFlowTestIds } from "App/overview/components/update-os-flow/update-os-flow-test-ids.enum" import { OnboardingNotCompleteModal } from "App/overview/components/onboarding-not-complete-modal" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" const UpdatingForceModalFlow: FunctionComponent< UpdatingForceModalFlowProps @@ -31,6 +32,7 @@ const UpdatingForceModalFlow: FunctionComponent< forceUpdateState, closeForceUpdateFlow, deviceType, + layer = ModalLayers.UpdateOS, }) => { const [forceUpdateShowModal, setForceUpdateShowModal] = useState(false) @@ -84,10 +86,12 @@ const UpdatingForceModalFlow: FunctionComponent< open={forceUpdateShowModal} releases={availableReleasesForUpdate ?? []} onUpdate={startForceUpdate} + layer={layer} /> {availableReleasesForUpdate && availableReleasesForUpdate.length > 0 && ( void openContactSupportFlow: () => void closeForceUpdateFlow: () => void + layer?: ModalLayers } diff --git a/packages/app/src/settings/actions/check-update-available.action.test.ts b/packages/app/src/settings/actions/check-update-available.action.test.ts index 29fecb5453..e7013f111e 100644 --- a/packages/app/src/settings/actions/check-update-available.action.test.ts +++ b/packages/app/src/settings/actions/check-update-available.action.test.ts @@ -7,6 +7,7 @@ import { AnyAction } from "@reduxjs/toolkit" import thunk from "redux-thunk" import createMockStore from "redux-mock-store" import { checkUpdateAvailable } from "./check-update-available.action" +import { setCheckingForUpdate } from "./set-checking-for-update-action" import checkAppUpdateRequest from "App/__deprecated__/renderer/requests/check-app-update.request" jest.mock("App/__deprecated__/renderer/requests/check-app-update.request") @@ -24,6 +25,7 @@ test("`checkUpdateAvailable` action dispatch SettingsEvent.CheckUpdateAvailable expect(mockStore.getActions()).toEqual([ checkUpdateAvailable.pending(requestId), + setCheckingForUpdate(true), checkUpdateAvailable.fulfilled(undefined, requestId, undefined), ]) expect(checkAppUpdateRequest).toHaveBeenCalled() diff --git a/packages/app/src/settings/actions/check-update-available.action.ts b/packages/app/src/settings/actions/check-update-available.action.ts index b9c9246603..4063191a9d 100644 --- a/packages/app/src/settings/actions/check-update-available.action.ts +++ b/packages/app/src/settings/actions/check-update-available.action.ts @@ -6,10 +6,12 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { SettingsEvent } from "App/settings/constants" import checkAppUpdateRequest from "App/__deprecated__/renderer/requests/check-app-update.request" +import { setCheckingForUpdate } from "App/settings/actions/set-checking-for-update-action" export const checkUpdateAvailable = createAsyncThunk( SettingsEvent.CheckUpdateAvailable, - async () => { + async (_, { dispatch }) => { + dispatch(setCheckingForUpdate(true)) await checkAppUpdateRequest() } ) diff --git a/packages/app/src/settings/actions/delete-collecting-data.action.ts b/packages/app/src/settings/actions/delete-collecting-data.action.ts new file mode 100644 index 0000000000..7992c4a93e --- /dev/null +++ b/packages/app/src/settings/actions/delete-collecting-data.action.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAsyncThunk } from "@reduxjs/toolkit" +import { SettingsEvent } from "App/settings/constants" +import { updateSettings } from "App/settings/requests" + +export const deleteCollectingData = createAsyncThunk( + SettingsEvent.DeleteCollectingData, + async () => { + await updateSettings({ key: "collectingData", value: undefined }) + + return + } +) diff --git a/packages/app/src/settings/actions/index.ts b/packages/app/src/settings/actions/index.ts index 6786a3c9e5..8b21e939e1 100644 --- a/packages/app/src/settings/actions/index.ts +++ b/packages/app/src/settings/actions/index.ts @@ -19,4 +19,6 @@ export * from "./set-os-updates.action" export * from "./set-settings.action" export * from "./toggle-tethering.action" export * from "./toggle-collection-data.action" +export * from "./toggle-privacy-policy-accepted.action" export * from "./toggle-application-update-available.action" +export * from "./set-checking-for-update-action" diff --git a/packages/app/src/settings/actions/load-settings.action.test.ts b/packages/app/src/settings/actions/load-settings.action.test.ts index 4fa19f1931..6a9c0db841 100644 --- a/packages/app/src/settings/actions/load-settings.action.test.ts +++ b/packages/app/src/settings/actions/load-settings.action.test.ts @@ -34,7 +34,6 @@ jest.mock("App/backup/actions/load-backup-data.action", () => ({ jest.mock("App/modals-manager/actions", () => ({ checkAppForcedUpdateFlowToShow: () => jest.fn(), - checkCollectingDataModalToShow: () => jest.fn(), checkAppUpdateFlowToShow: () => jest.fn(), })) @@ -63,6 +62,7 @@ test("`loadSettings` action dispatch SettingsEvent.LoadSettings event and calls { type: SettingsEvent.SetSettings, payload: { + checkingForUpdate: false, collectingData: false, currentVersion: `${packageInfo.version}`, lowestSupportedVersions: { diff --git a/packages/app/src/settings/actions/load-settings.action.ts b/packages/app/src/settings/actions/load-settings.action.ts index f2bd9d8eb6..59501b2d6a 100644 --- a/packages/app/src/settings/actions/load-settings.action.ts +++ b/packages/app/src/settings/actions/load-settings.action.ts @@ -10,7 +10,6 @@ import { getSettings } from "App/settings/requests" import { loadBackupData } from "App/backup/actions/load-backup-data.action" import { checkAppForcedUpdateFlowToShow, - checkCollectingDataModalToShow, checkAppUpdateFlowToShow, } from "App/modals-manager/actions" import { setSettings } from "App/settings/actions/set-settings.action" @@ -47,6 +46,7 @@ export const loadSettings = createAsyncThunk( ...settings, updateRequired, currentVersion: packageInfo.version, + checkingForUpdate: false, lowestSupportedVersions: { lowestSupportedCenterVersion: configuration.centerVersion, lowestSupportedProductVersion: configuration.productVersions, @@ -56,7 +56,6 @@ export const loadSettings = createAsyncThunk( void dispatch(loadBackupData()) void dispatch(checkAppUpdateFlowToShow()) void dispatch(checkAppForcedUpdateFlowToShow()) - void dispatch(checkCollectingDataModalToShow()) return } diff --git a/packages/app/src/settings/actions/set-checking-for-update-action.ts b/packages/app/src/settings/actions/set-checking-for-update-action.ts new file mode 100644 index 0000000000..31576fa3e1 --- /dev/null +++ b/packages/app/src/settings/actions/set-checking-for-update-action.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAction } from "@reduxjs/toolkit" +import { SettingsEvent } from "App/settings/constants" + +export const setCheckingForUpdate = createAction( + SettingsEvent.SetCheckingForUpdate +) diff --git a/packages/app/src/settings/actions/set-settings.action.test.ts b/packages/app/src/settings/actions/set-settings.action.test.ts index b330d9c3cb..3df94d8d87 100644 --- a/packages/app/src/settings/actions/set-settings.action.test.ts +++ b/packages/app/src/settings/actions/set-settings.action.test.ts @@ -30,6 +30,7 @@ const settings: Omit< language: "en-US", neverConnected: true, collectingData: false, + privacyPolicyAccepted: false, diagnosticSentTimestamp: 0, ignoredCrashDumps: [], updateRequired: false, @@ -39,8 +40,10 @@ const settings: Omit< lowestSupportedProductVersion: { MuditaHarmony: "1.5.0", MuditaPure: "1.0.0", + MuditaKompakt: "2.0.0", }, }, + checkingForUpdate: false, } const mockStore = createMockStore([thunk])() diff --git a/packages/app/src/settings/actions/toggle-collection-data.action.test.ts b/packages/app/src/settings/actions/toggle-collection-data.action.test.ts index a049b4a8a8..1005a35a4a 100644 --- a/packages/app/src/settings/actions/toggle-collection-data.action.test.ts +++ b/packages/app/src/settings/actions/toggle-collection-data.action.test.ts @@ -6,20 +6,11 @@ import { AnyAction } from "@reduxjs/toolkit" import thunk from "redux-thunk" import createMockStore from "redux-mock-store" -import { toggleTrackingRequest } from "App/analytic-data-tracker/requests" import { updateSettings } from "App/settings/requests" import { toggleCollectionData } from "./toggle-collection-data.action" -import logger from "App/__deprecated__/main/utils/logger" const mockStore = createMockStore([thunk])() -jest.mock("App/__deprecated__/main/utils/logger", () => ({ - enableRollbar: jest.fn(), - disableRollbar: jest.fn(), -})) -jest.mock("App/analytic-data-tracker/requests", () => ({ - toggleTrackingRequest: jest.fn(), -})) jest.mock("App/settings/requests", () => ({ updateSettings: jest.fn(), })) @@ -29,10 +20,8 @@ afterEach(() => { mockStore.clearActions() }) -test("enables `logging/tracking` functionalities and class `updateSettings` with `true` value", async () => { +test("calls `updateSettings` with `true` value", async () => { expect(updateSettings).not.toHaveBeenCalled() - expect(toggleTrackingRequest).not.toHaveBeenCalled() - expect(logger.enableRollbar).not.toHaveBeenCalled() const { meta: { requestId }, @@ -46,18 +35,14 @@ test("enables `logging/tracking` functionalities and class `updateSettings` with toggleCollectionData.pending(requestId, true), toggleCollectionData.fulfilled(true, requestId, true), ]) - expect(logger.enableRollbar).toHaveBeenCalled() - expect(toggleTrackingRequest).toHaveBeenCalledWith(true) expect(updateSettings).toHaveBeenCalledWith({ key: "collectingData", value: true, }) }) -test("disenables `logging/tracking` functionalities and class `updateSettings` with `false` value", async () => { +test("calls `updateSettings` with `false` value", async () => { expect(updateSettings).not.toHaveBeenCalled() - expect(toggleTrackingRequest).not.toHaveBeenCalled() - expect(logger.disableRollbar).not.toHaveBeenCalled() const { meta: { requestId }, @@ -71,8 +56,6 @@ test("disenables `logging/tracking` functionalities and class `updateSettings` w toggleCollectionData.pending(requestId, false), toggleCollectionData.fulfilled(false, requestId, false), ]) - expect(logger.disableRollbar).toHaveBeenCalled() - expect(toggleTrackingRequest).toHaveBeenCalledWith(false) expect(updateSettings).toHaveBeenCalledWith({ key: "collectingData", value: false, diff --git a/packages/app/src/settings/actions/toggle-collection-data.action.ts b/packages/app/src/settings/actions/toggle-collection-data.action.ts index 7e824dd132..c500dcdcf6 100644 --- a/packages/app/src/settings/actions/toggle-collection-data.action.ts +++ b/packages/app/src/settings/actions/toggle-collection-data.action.ts @@ -5,15 +5,11 @@ import { createAsyncThunk } from "@reduxjs/toolkit" import { SettingsEvent } from "App/settings/constants" -import { toggleTrackingRequest } from "App/analytic-data-tracker/requests" import { updateSettings } from "App/settings/requests" -import logger from "App/__deprecated__/main/utils/logger" export const toggleCollectionData = createAsyncThunk( SettingsEvent.ToggleCollectionData, async (payload) => { - payload ? logger.enableRollbar() : logger.disableRollbar() - await toggleTrackingRequest(payload) await updateSettings({ key: "collectingData", value: payload }) return payload diff --git a/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.test.ts b/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.test.ts new file mode 100644 index 0000000000..29b1a5e1cf --- /dev/null +++ b/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.test.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { AnyAction } from "@reduxjs/toolkit" +import thunk from "redux-thunk" +import createMockStore from "redux-mock-store" +import { toggleTrackingRequest } from "App/analytic-data-tracker/requests" +import { updateSettings } from "App/settings/requests" +import { togglePrivacyPolicyAccepted } from "./toggle-privacy-policy-accepted.action" +import logger from "App/__deprecated__/main/utils/logger" + +const mockStore = createMockStore([thunk])() + +jest.mock("App/__deprecated__/main/utils/logger", () => ({ + enableRollbar: jest.fn(), + disableRollbar: jest.fn(), +})) +jest.mock("App/analytic-data-tracker/requests", () => ({ + toggleTrackingRequest: jest.fn(), +})) +jest.mock("App/settings/requests", () => ({ + updateSettings: jest.fn(), +})) + +afterEach(() => { + jest.resetAllMocks() + mockStore.clearActions() +}) + +test("enables `logging/tracking` functionalities and calls `updateSettings` with `true` value", async () => { + expect(updateSettings).not.toHaveBeenCalled() + expect(toggleTrackingRequest).not.toHaveBeenCalled() + expect(logger.enableRollbar).not.toHaveBeenCalled() + + const { + meta: { requestId }, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/await-thenable + } = await mockStore.dispatch( + togglePrivacyPolicyAccepted(true) as unknown as AnyAction + ) + + expect(mockStore.getActions()).toEqual([ + togglePrivacyPolicyAccepted.pending(requestId, true), + togglePrivacyPolicyAccepted.fulfilled(true, requestId, true), + ]) + expect(logger.enableRollbar).toHaveBeenCalled() + expect(toggleTrackingRequest).toHaveBeenCalledWith(true) + expect(updateSettings).toHaveBeenCalledWith({ + key: "privacyPolicyAccepted", + value: true, + }) +}) + +test("disables `logging/tracking` functionalities and calls `updateSettings` with `false` value", async () => { + expect(updateSettings).not.toHaveBeenCalled() + expect(toggleTrackingRequest).not.toHaveBeenCalled() + expect(logger.disableRollbar).not.toHaveBeenCalled() + + const { + meta: { requestId }, + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/await-thenable + } = await mockStore.dispatch( + togglePrivacyPolicyAccepted(false) as unknown as AnyAction + ) + + expect(mockStore.getActions()).toEqual([ + togglePrivacyPolicyAccepted.pending(requestId, false), + togglePrivacyPolicyAccepted.fulfilled(false, requestId, false), + ]) + expect(logger.disableRollbar).toHaveBeenCalled() + expect(toggleTrackingRequest).toHaveBeenCalledWith(false) + expect(updateSettings).toHaveBeenCalledWith({ + key: "privacyPolicyAccepted", + value: false, + }) +}) diff --git a/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.ts b/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.ts new file mode 100644 index 0000000000..9bb1a33dd7 --- /dev/null +++ b/packages/app/src/settings/actions/toggle-privacy-policy-accepted.action.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { createAsyncThunk } from "@reduxjs/toolkit" +import { SettingsEvent } from "App/settings/constants" +import { toggleTrackingRequest } from "App/analytic-data-tracker/requests" +import { updateSettings } from "App/settings/requests" +import logger from "App/__deprecated__/main/utils/logger" + +export const togglePrivacyPolicyAccepted = createAsyncThunk( + SettingsEvent.TogglePrivacyPolicyAccepted, + async (payload) => { + payload ? logger.enableRollbar() : logger.disableRollbar() + await toggleTrackingRequest(payload) + await updateSettings({ key: "privacyPolicyAccepted", value: payload }) + + return payload + } +) diff --git a/packages/app/src/settings/components/about/about-loader.component.tsx b/packages/app/src/settings/components/about/about-loader.component.tsx new file mode 100644 index 0000000000..02fa5a8053 --- /dev/null +++ b/packages/app/src/settings/components/about/about-loader.component.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React from "react" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import { defineMessages } from "react-intl" +import LoaderModal from "App/ui/components/loader-modal/loader-modal.component" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { ModalDialogProps } from "App/ui" + +const messages = defineMessages({ + loadingTitle: { + id: "module.settings.loadingTitle", + }, + loadingSubtitle: { + id: "module.settings.loadingSubtitle", + }, +}) + +interface Props extends Omit { + testId?: string + open: boolean +} + +export const AboutLoaderModal: FunctionComponent = ({ + testId, + open, + ...rest +}) => { + return ( + + ) +} diff --git a/packages/app/src/settings/components/about/about-ui.component.tsx b/packages/app/src/settings/components/about/about-ui.component.tsx index 3697bbf834..0b018fab2d 100644 --- a/packages/app/src/settings/components/about/about-ui.component.tsx +++ b/packages/app/src/settings/components/about/about-ui.component.tsx @@ -3,7 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React from "react" +import React, { useState, useEffect } from "react" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import { ActionsWrapper } from "App/__deprecated__/renderer/components/rest/messages/threads-table.component" import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" @@ -25,6 +25,11 @@ import styled from "styled-components" import Text from "App/__deprecated__/renderer/components/core/text/text.component" import { borderColor } from "App/__deprecated__/renderer/styles/theming/theme-getters" import { AppUpdateNotAvailable } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update.modals" +import { UpdateFailedModal } from "App/settings/components/about/update-failed-modal.component" +import { AboutLoaderModal } from "App/settings/components/about/about-loader.component" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" +import { useOnlineChecker } from "App/settings/hooks/use-online-checker" +import registerErrorAppUpdateListener from "App/__deprecated__/main/functions/register-error-app-update-listener" const AvailableUpdate = styled(Text)` margin-top: 0.8rem; @@ -63,6 +68,9 @@ interface Props { appUpdateNotAvailableShow?: boolean onAppUpdateAvailableCheck: () => void hideAppUpdateNotAvailable: () => void + checkingForUpdate: boolean + appUpdateFailedShow: boolean + hideAppUpdateFailed: () => void } const AboutUI: FunctionComponent = ({ @@ -75,113 +83,175 @@ const AboutUI: FunctionComponent = ({ appUpdateAvailable, onAppUpdateAvailableCheck, hideAppUpdateNotAvailable, -}) => ( - <> - - - - - - - - - {appUpdateAvailable ? ( - - + checkingForUpdate, + appUpdateFailedShow, + hideAppUpdateFailed, +}) => { + const [updateCheck, setUpdateCheck] = useState(false) + const online = useOnlineChecker() + + const appUpdateAvailableCheckHandler = () => { + setUpdateCheck(true) + onAppUpdateAvailableCheck() + } + const hideAppUpdateNotAvailableHandler = () => { + setUpdateCheck(false) + hideAppUpdateNotAvailable() + } + const hideAppUpdateFailedHandler = () => { + setUpdateCheck(false) + hideAppUpdateNotAvailable() + hideAppUpdateFailed() + } + + const showUpToDateModal = + updateCheck && !appUpdateFailedShow && !checkingForUpdate + + useEffect(() => { + const unregister = registerErrorAppUpdateListener(() => { + setUpdateCheck(false) + }) + return () => unregister() + }, []) + + return ( + <> + + + {showUpToDateModal && ( + + )} + + + + + + + {!online && ( + + + + + + + )} + {appUpdateAvailable && online && ( + + + + + - + + )} + {!appUpdateAvailable && online && ( + + + + + + + )} + + + + + + + + + + + + + + + + + + - - ) : ( - - - - + + + + + + + + + - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -) + + + + + ) +} export default AboutUI diff --git a/packages/app/src/settings/components/about/about.component.tsx b/packages/app/src/settings/components/about/about.component.tsx index 3f05d9effb..f1189b3a5e 100644 --- a/packages/app/src/settings/components/about/about.component.tsx +++ b/packages/app/src/settings/components/about/about.component.tsx @@ -3,7 +3,7 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -import React, { useState } from "react" +import React, { useState, useEffect } from "react" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import AboutUI from "App/settings/components/about/about-ui.component" import { ipcRenderer } from "electron-better-ipc" @@ -13,18 +13,21 @@ interface Props { latestVersion?: string currentVersion?: string updateAvailable?: boolean + checkingForUpdate: boolean checkUpdateAvailable: () => void - toggleUpdateAvailable: (appUpdateAvailable: boolean) => void } export const About: FunctionComponent = ({ latestVersion, currentVersion, updateAvailable, + checkingForUpdate, checkUpdateAvailable, - toggleUpdateAvailable, }) => { - const [checking, setChecking] = useState(false) + const [appUpdateNotAvailableShow, setAppUpdateNotAvailableShow] = + useState(false) + const [appUpdateFailedShow, setAppUpdateFailedShow] = useState(false) + const openLicenseWindow = () => ipcRenderer.callMain(AboutActions.LicenseOpenWindow) const openTermsOfServiceWindow = () => @@ -33,13 +36,20 @@ export const About: FunctionComponent = ({ ipcRenderer.callMain(AboutActions.PolicyOpenWindow) const hideAppUpdateNotAvailable = () => { - setChecking(false) + setAppUpdateNotAvailableShow(false) + } + + const hideAppUpdateFailed = () => { + setAppUpdateFailedShow(false) } + useEffect(() => { + setAppUpdateNotAvailableShow(updateAvailable === false) + }, [updateAvailable, checkingForUpdate]) + const handleAppUpdateAvailableCheck = (): void => { - setChecking(true) if (!window.navigator.onLine) { - toggleUpdateAvailable(false) + setAppUpdateFailedShow(true) } else { checkUpdateAvailable() } @@ -54,8 +64,11 @@ export const About: FunctionComponent = ({ appCurrentVersion={currentVersion} appUpdateAvailable={updateAvailable} onAppUpdateAvailableCheck={handleAppUpdateAvailableCheck} - appUpdateNotAvailableShow={checking && updateAvailable === false} + appUpdateNotAvailableShow={appUpdateNotAvailableShow} hideAppUpdateNotAvailable={hideAppUpdateNotAvailable} + checkingForUpdate={checkingForUpdate} + appUpdateFailedShow={appUpdateFailedShow} + hideAppUpdateFailed={hideAppUpdateFailed} /> ) } diff --git a/packages/app/src/settings/components/about/about.container.tsx b/packages/app/src/settings/components/about/about.container.tsx index c7fd214078..226b158bbe 100644 --- a/packages/app/src/settings/components/about/about.container.tsx +++ b/packages/app/src/settings/components/about/about.container.tsx @@ -6,21 +6,18 @@ import { connect } from "react-redux" import { About } from "App/settings/components/about/about.component" import { ReduxRootState, RootState } from "App/__deprecated__/renderer/store" -import { - toggleApplicationUpdateAvailable, - checkUpdateAvailable, -} from "App/settings/actions" +import { checkUpdateAvailable } from "App/settings/actions" const mapStateToProps = (state: RootState & ReduxRootState) => { return { latestVersion: state.settings.latestVersion, currentVersion: state.settings.currentVersion, updateAvailable: state.settings.updateAvailable, + checkingForUpdate: state.settings.checkingForUpdate, } } const mapDispatchToProps = { - toggleApplicationUpdateAvailable, checkUpdateAvailable, } diff --git a/packages/app/src/settings/components/about/about.stories.tsx b/packages/app/src/settings/components/about/about.stories.tsx index b8e0663285..62e8410e9d 100644 --- a/packages/app/src/settings/components/about/about.stories.tsx +++ b/packages/app/src/settings/components/about/about.stories.tsx @@ -16,6 +16,9 @@ storiesOf("Settings/About", module).add("About", () => ( openPrivacyPolicy={noop} hideAppUpdateNotAvailable={noop} onAppUpdateAvailableCheck={noop} + checkingForUpdate={false} + appUpdateFailedShow={false} + hideAppUpdateFailed={noop} />
)) diff --git a/packages/app/src/settings/components/about/about.test.tsx b/packages/app/src/settings/components/about/about.test.tsx index 7837de3817..4f8a5fd06c 100644 --- a/packages/app/src/settings/components/about/about.test.tsx +++ b/packages/app/src/settings/components/about/about.test.tsx @@ -8,12 +8,15 @@ import "@testing-library/jest-dom/extend-expect" import React, { ComponentProps } from "react" import AboutUI from "./about-ui.component" import { noop } from "App/__deprecated__/renderer/utils/noop" -import { AboutTestIds } from "./about.enum" +import { AboutTestIds } from "App/settings/components/about/about.enum" import { fireEvent, screen } from "@testing-library/dom" import { AppUpdateStepModalTestIds } from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal-test-ids.enum" import { flags } from "App/feature-flags" jest.mock("App/feature-flags") +jest.mock( + "App/__deprecated__/main/functions/register-error-app-update-listener" +) type Props = ComponentProps const defaultProps: Props = { @@ -26,6 +29,9 @@ const defaultProps: Props = { appCurrentVersion: "0.19.0", appUpdateAvailable: true, appUpdateNotAvailableShow: false, + checkingForUpdate: false, + appUpdateFailedShow: false, + hideAppUpdateFailed: noop, } const renderer = (extraProps?: Partial) => { @@ -52,12 +58,14 @@ test("renders at least one table row", () => { }) test("Opens update modal properly when app update is not available", () => { - renderer({ + const { getByTestId } = renderer({ appLatestVersion: "0.20.2", appCurrentVersion: "0.20.2", appUpdateNotAvailableShow: true, }) + getByTestId(AboutTestIds.UpdateButton).click() + expect( screen.getByTestId(AppUpdateStepModalTestIds.AppUpdateNotAvailableModal) ).toBeInTheDocument() diff --git a/packages/app/src/settings/components/about/shared.tsx b/packages/app/src/settings/components/about/shared.tsx index b01a242669..3d2433b542 100644 --- a/packages/app/src/settings/components/about/shared.tsx +++ b/packages/app/src/settings/components/about/shared.tsx @@ -26,7 +26,29 @@ export const LightText = styled(Text)` margin-bottom: 1.6rem; ` +export const BoldText = styled(Text)` + font-weight: normal; + margin-bottom: 1.6rem; +` + export const LightTextNested = styled(Text)` margin-bottom: 1.6rem; margin-left: 3rem; ` +export const GridWrapper = styled("div")` + display: grid; + grid-template-columns: 3fr 2fr; + grid-auto-rows: minmax(10rem, auto); + margin-bottom: 1.6rem; +` + +export const GridItem = styled(Text)` + display: flex; + align-items: center; + padding: 1rem 0.5rem 1rem 0.5rem; + border: 0.125rem solid black; + margin: 0 -0.125rem -0.125rem 0; +` +export const GridBoldItem = styled(GridItem)` + font-weight: normal; +` diff --git a/packages/app/src/settings/components/about/update-failed-modal.component.tsx b/packages/app/src/settings/components/about/update-failed-modal.component.tsx new file mode 100644 index 0000000000..5f25fa9f7e --- /dev/null +++ b/packages/app/src/settings/components/about/update-failed-modal.component.tsx @@ -0,0 +1,50 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import React from "react" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import ErrorModal from "App/ui/components/error-modal/error-modal.component" +import { defineMessages } from "react-intl" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { ModalDialogProps } from "App/ui" + +const messages = defineMessages({ + checkForFailedAppUpdateTitle: { + id: "module.settings.checkForFailedAppUpdateTitle", + }, + checkForFailedAppUpdateSubtitle: { + id: "module.settings.checkForFailedAppUpdateSubtitle", + }, + checkForFailedAppUpdateBody: { + id: "module.settings.checkForFailedAppUpdateBody", + }, +}) + +interface Props extends Omit { + testId?: string + open: boolean + closeModal: () => void +} + +export const UpdateFailedModal: FunctionComponent = ({ + testId, + open, + closeModal, + ...rest +}) => { + return ( + + ) +} diff --git a/packages/app/src/settings/components/app-forced-update-flow/app-forced-update-flow.component.tsx b/packages/app/src/settings/components/app-forced-update-flow/app-forced-update-flow.component.tsx index 7b4fe92809..6c9d4a908d 100644 --- a/packages/app/src/settings/components/app-forced-update-flow/app-forced-update-flow.component.tsx +++ b/packages/app/src/settings/components/app-forced-update-flow/app-forced-update-flow.component.tsx @@ -7,6 +7,7 @@ import React from "react" import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" import AppUpdateStepModal from "App/__deprecated__/renderer/wrappers/app-update-step-modal/app-update-step-modal.component" import { AppForcedUpdateFlowTestIds } from "App/settings/components/app-forced-update-flow/app-forced-update-flow-test-ids.enum" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" interface Props { appCurrentVersion?: string @@ -22,6 +23,7 @@ export const AppForcedUpdateFlow: FunctionComponent = ({ return ( = ({ }) => { return ( - -const defaultProps: Props = { - open: false, - onActionButtonClick: noop, - closeModal: noop, - onFullAgreementButtonClick: noop, -} -const render = (extraProps?: Partial) => { - const props = { - ...defaultProps, - ...extraProps, - } - return renderWithThemeAndIntl() -} - -describe("`CollectingDataModalUi` component", () => { - describe("when component is render with default props", () => { - test("`CollectingDataModalUi` isn't visible", () => { - const { queryByTestId } = render() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() - }) - }) - - describe("when component`open` is set to `true`", () => { - test("`CollectingDataModalUi` is visible", () => { - const { queryByTestId } = render({ open: true }) - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).toBeInTheDocument() - }) - }) -}) diff --git a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal-ui.component.tsx b/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal-ui.component.tsx deleted file mode 100644 index 49b94fed50..0000000000 --- a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal-ui.component.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import React, { ComponentProps } from "react" -import { intl } from "App/__deprecated__/renderer/utils/intl" -import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" -import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" -import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" -import { defineMessages } from "react-intl" -import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { CollectingDataModalTestIds } from "App/settings/components/collecting-data-modal/collecting-data-modal-test-ids.enum" -import { - FullAgreementButton, - ModalContent, - Paragraph, -} from "App/settings/components/collecting-data-modal/collecting-data-modal.styled" -import { ModalDialog } from "App/ui/components/modal-dialog" -import { DisplayStyle } from "App/__deprecated__/renderer/components/core/button/button.config" -import { Size } from "App/__deprecated__/renderer/components/core/button/button.config" -import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" - -const messages = defineMessages({ - title: { id: "component.collectingDataModalTitle" }, - text: { id: "component.collectingDataModalText" }, - body: { id: "component.collectingDataModalBody" }, - cancelButton: { id: "component.collectingDataModalCancel" }, - agreeButton: { id: "component.collectingDataModalAgree" }, -}) - -interface Props - extends Required< - Pick< - ComponentProps, - "onActionButtonClick" | "closeModal" | "open" - > - > { - onFullAgreementButtonClick: () => void -} - -export const CollectingDataModalUi: FunctionComponent = ({ - onFullAgreementButtonClick, - ...props -}) => { - return ( - - - - - - - - - ) -} diff --git a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.test.tsx b/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.test.tsx deleted file mode 100644 index 525da709ca..0000000000 --- a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import React, { ComponentProps } from "react" -import { renderWithThemeAndIntl } from "App/__deprecated__/renderer/utils/render-with-theme-and-intl" -import { CollectingDataModal } from "App/settings/components/collecting-data-modal/collecting-data-modal.component" -import { CollectingDataModalTestIds } from "App/settings/components/collecting-data-modal/collecting-data-modal-test-ids.enum" -import { noop } from "App/__deprecated__/renderer/utils/noop" - -type Props = ComponentProps - -const defaultProps: Props = { - open: false, - toggleCollectionData: noop, - closeModal: noop, -} -const render = (extraProps?: Partial) => { - const props = { - ...defaultProps, - ...extraProps, - } - return renderWithThemeAndIntl() -} - -describe("`CollectingDataModal` component", () => { - describe("when component is render with default props", () => { - test("`CollectingDataModal` isn't visible", () => { - const { queryByTestId } = render() - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).not.toBeInTheDocument() - }) - }) - - describe("when component`open` is set to `true`", () => { - test("`CollectingDataModal` is visible", () => { - const { queryByTestId } = render({ open: true }) - expect( - queryByTestId(CollectingDataModalTestIds.Container) - ).toBeInTheDocument() - }) - }) -}) diff --git a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.tsx b/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.tsx deleted file mode 100644 index 1dd169f547..0000000000 --- a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.component.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import React, { ComponentProps } from "react" -import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { ipcRenderer } from "electron-better-ipc" -import { AboutActions } from "App/__deprecated__/common/enums/about-actions.enum" -import { CollectingDataModalUi } from "App/settings/components/collecting-data-modal/collecting-data-modal-ui.component" - -interface Props - extends Pick< - ComponentProps, - "closeModal" | "open" - > { - toggleCollectionData: (flag: boolean) => void -} - -export const CollectingDataModal: FunctionComponent = ({ - open, - toggleCollectionData, - closeModal, -}) => { - const openPrivacyPolicy = () => - ipcRenderer.callMain(AboutActions.PolicyOpenWindow) - - const allowToAppCollectingData = (): void => { - toggleCollectionData(true) - closeModal() - } - - const disallowToAppCollectingData = (): void => { - toggleCollectionData(false) - closeModal() - } - - return ( - - ) -} diff --git a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.container.tsx b/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.container.tsx deleted file mode 100644 index 137bf5932f..0000000000 --- a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.container.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { connect } from "react-redux" -import { TmpDispatch } from "App/__deprecated__/renderer/store" -import { CollectingDataModal } from "App/settings/components/collecting-data-modal/collecting-data-modal.component" -import { hideCollectingDataModal } from "App/modals-manager/actions" -import { toggleCollectionData } from "App/settings/actions" - -const mapDispatchToProps = (dispatch: TmpDispatch) => ({ - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - closeModal: () => dispatch(hideCollectingDataModal()), - toggleCollectionData: (value: boolean) => - // AUTO DISABLED - fix me if you like :) - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - dispatch(toggleCollectionData(value)), -}) - -export const CollectingDataModalContainer = connect( - undefined, - mapDispatchToProps -)(CollectingDataModal) diff --git a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.styled.tsx b/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.styled.tsx deleted file mode 100644 index f9b4f38eea..0000000000 --- a/packages/app/src/settings/components/collecting-data-modal/collecting-data-modal.styled.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import styled from "styled-components" -import Text from "App/__deprecated__/renderer/components/core/text/text.component" -import ButtonComponent from "App/__deprecated__/renderer/components/core/button/button.component" - -export const ModalContent = styled.div` - display: flex; - flex-direction: column; - align-items: center; -` - -export const Paragraph = styled(Text)` - white-space: pre-wrap; - text-align: center; - line-height: 2.2rem; - margin-top: 1.2rem; -` -export const FullAgreementButton = styled(ButtonComponent)` - width: auto; - height: 2rem; - padding: 0; - &:hover { - background-color: transparent; - } - - p { - text-transform: none; - } -` diff --git a/packages/app/src/settings/components/index.ts b/packages/app/src/settings/components/index.ts index b06a2ea470..55c3522396 100644 --- a/packages/app/src/settings/components/index.ts +++ b/packages/app/src/settings/components/index.ts @@ -9,7 +9,6 @@ export * from "./audio-conversion" export * from "./app-update-flow" export * from "./audio-conversion-radio-group" export * from "./backup" -export * from "./collecting-data-modal" export * from "./notifications" export * from "./privacy-policy" export * from "./settings" diff --git a/packages/app/src/settings/components/notifications/__snapshots__/notifications.test.tsx.snap b/packages/app/src/settings/components/notifications/__snapshots__/notifications.test.tsx.snap index 673919d4dd..1df1badeb3 100644 --- a/packages/app/src/settings/components/notifications/__snapshots__/notifications.test.tsx.snap +++ b/packages/app/src/settings/components/notifications/__snapshots__/notifications.test.tsx.snap @@ -142,6 +142,48 @@ exports[`matches snapshot 1`] = ` white-space: nowrap; } +.c4 { + display: grid; + box-sizing: border-box; + grid-template-areas: "Checkbox Actions"; + grid-template-columns: 1fr 16.4rem; + border-bottom: solid 0.1rem #d2d6db; + height: 7.2rem; + max-height: 7.2rem; +} + +.c5 { + grid-area: Checkbox; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c7 { + margin-left: 3.2rem; + margin-bottom: 0; + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + grid-area: Label; +} + +.c1 { + border-bottom: solid 0.1rem #d2d6db; +} + +.c3 { + margin-left: 3.2rem; + margin-bottom: 3.2rem; +} + .c9 { display: -webkit-box; display: -webkit-flex; @@ -246,48 +288,6 @@ exports[`matches snapshot 1`] = ` width: 8.8rem; } -.c4 { - display: grid; - box-sizing: border-box; - grid-template-areas: "Checkbox Actions"; - grid-template-columns: 1fr 16.4rem; - border-bottom: solid 0.1rem #d2d6db; - height: 7.2rem; - max-height: 7.2rem; -} - -.c5 { - grid-area: Checkbox; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; -} - -.c7 { - margin-left: 3.2rem; - margin-bottom: 0; - -webkit-align-self: center; - -ms-flex-item-align: center; - align-self: center; - grid-area: Label; -} - -.c1 { - border-bottom: solid 0.1rem #d2d6db; -} - -.c3 { - margin-left: 3.2rem; - margin-bottom: 3.2rem; -} - .c0 { padding-top: 3.2rem; } diff --git a/packages/app/src/settings/components/privacy-policy-modal/privacy-policy-modal.component.tsx b/packages/app/src/settings/components/privacy-policy-modal/privacy-policy-modal.component.tsx new file mode 100644 index 0000000000..b54098bfb2 --- /dev/null +++ b/packages/app/src/settings/components/privacy-policy-modal/privacy-policy-modal.component.tsx @@ -0,0 +1,115 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { + ModalContent, + ModalDialog, + RoundIconWrapper, +} from "App/ui/components/modal-dialog" +import { ModalSize } from "App/__deprecated__/renderer/components/core/modal/modal.interface" +import Icon from "App/__deprecated__/renderer/components/core/icon/icon.component" + +import React, { useEffect } from "react" +import { IconType } from "App/__deprecated__/renderer/components/core/icon/icon-type" +import Text from "App/__deprecated__/renderer/components/core/text/text.component" +import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" +import styled from "styled-components" +import { + fontWeight, + textColor, +} from "App/__deprecated__/renderer/styles/theming/theme-getters" +import { defineMessages } from "react-intl" +import { intl } from "App/__deprecated__/renderer/utils/intl" +import { ipcRenderer } from "electron-better-ipc" +import { AboutActions } from "App/__deprecated__/common/enums/about-actions.enum" +import { useDispatch, useSelector } from "react-redux" +import { togglePrivacyPolicyAccepted } from "App/settings/actions" +import { Dispatch, ReduxRootState } from "App/__deprecated__/renderer/store" +import { deleteCollectingData } from "App/settings/actions/delete-collecting-data.action" +import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" +import { ModalLayers } from "App/modals-manager/constants/modal-layers.enum" + +const messages = defineMessages({ + privacyPolicyModalTitle: { id: "module.settings.privacyPolicyModalTitle" }, + privacyPolicyModalHeader: { id: "module.settings.privacyPolicyModalHeader" }, + privacyPolicyModalDescription: { + id: "module.settings.privacyPolicyModalDescription", + }, + privacyPolicyModalLink: { id: "module.settings.privacyPolicyModalLink" }, + privacyPolicyModalButton: { id: "module.settings.privacyPolicyModalButton" }, +}) + +const PrivacyPolicyLink = styled.a` + text-decoration: underline; + cursor: pointer; + font-size: 1.4rem; + font-weight: ${fontWeight("default")}; + color: ${textColor("action")}; +` + +export const DescriptionText = styled(Text)` + text-align: center; + margin: 0.8rem 0 2.2rem 0; +` + +const PrivacyPolicyModal: FunctionComponent = () => { + const { collectingData } = useSelector( + (state: ReduxRootState) => state.settings + ) + + const dispatch = useDispatch() + + const openPrivacyPolicyWindow = () => + ipcRenderer.callMain(AboutActions.PolicyOpenBrowser) + + const handleAgreeButtonClick = (): void => { + void dispatch(deleteCollectingData()) + void dispatch(togglePrivacyPolicyAccepted(true)) + } + + useEffect(() => { + if (collectingData === undefined) { + void dispatch(togglePrivacyPolicyAccepted(true)) + } + }, [collectingData, dispatch]) + + if (collectingData === undefined) { + return null + } + + return ( + { + close() + }} + > + + + + + + + + {intl.formatMessage(messages.privacyPolicyModalLink)} + + + + ) +} + +export default PrivacyPolicyModal diff --git a/packages/app/src/settings/components/privacy-policy/privacy-policy-ui.component.tsx b/packages/app/src/settings/components/privacy-policy/privacy-policy-ui.component.tsx index 59be2a3f4c..e1f7b5cea5 100644 --- a/packages/app/src/settings/components/privacy-policy/privacy-policy-ui.component.tsx +++ b/packages/app/src/settings/components/privacy-policy/privacy-policy-ui.component.tsx @@ -10,9 +10,12 @@ import { PrivacyPolicyComponentTestIds } from "./privacy-policy-ui.enum" import { WindowContainer, WindowHeader, - WindowTitle, LightText, LightTextNested, + BoldText, + GridWrapper, + GridItem, + GridBoldItem, } from "App/settings/components/about/shared" export const PrivacyPolicyUI: FunctionComponent = () => ( @@ -23,248 +26,426 @@ export const PrivacyPolicyUI: FunctionComponent = () => ( > Mudita Center Privacy Policy - - Privacy policy - rules for the processing and protection of personal data - in Mudita sp. z o.o. - - For the purpose of the implementation of the requirements of Regulation - (EU) 2016/679 of the European Parliament and of the Council of 27 April - 2016 on the protection of natural persons with regard to the processing of - personal data and on the free movement of such data, and repealing - Directive 95/46/EC (General Data Protection Regulation), hereinafter - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - referred to as "GDPR", we inform you about the rules of processing your - personal data and about your rights related with it. + We hereby inform that we process your personal data. Details regarding + this can be found below. + + Who is the controller of your personal data and who can you contact about + it? + - The following rules apply from June 22, 2021. + 1. The Controller of your personal data is Mudita sp. z o.o. with + its registered office in Warsaw at ul. Jana Czeczota 6, 02-607 Warsaw, + entered into the Register of Entrepreneurs held by the Regional Court for + the Capital City of Warsaw, 12th Commercial Division of the National Court + Register, under KRS [National Court Register Number] 0000467620, NIP + [Polish Taxpayer Identification Number] 5252558282, REGON [National + Business Registration Number] 146767613, share capital:600.000,00 zł., + hereinafter referred to as “Controller or Mudita”. - 1. The controller of your personal data. The controller of your - personal data is Mudita sp. z o.o., Jana Czeczota Street No.6, - 02-607 Warsaw, Poland, REGON: 146767613, NIP (Tax No.): 5252558282, - entered into the entrepreneur register of the National Court Register by - the District Court for the Capital City of Warsaw, XII Commercial Division - of the National Court Register (KRS) under No.: 0000467620, share capital - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - (fully paid) PLN 20 000 (hereinafter referred to as "We”, " - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - Mudita"). Mudita distributes a desktop application called Mudita - Center (hereinafter “Center”). + 2. In cases regarding the protection of your personal data and the + exercising your rights you can contact us by e-mail: office@mudita.com or + in writing to our address indicated in clause 1. + + For what purposes and on what grounds do we process your personal data? + - 2. Contact details Mudita has set one contact point for all - personal data issues. If you would like to contact us, please write us an - e-mail to: office@mudita.com or - send a letter to: Mudita sp. z o.o., Jana Czeczota Street No. 6, 02-607 - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - Warsaw with the note: "Personal data". + 3. Your personal data is processed for the following purposes: + + + the purpose of the processing + + + legal basis for the processing + + + Mudita's software users + + + + sharing and enabling the use of the offered software + + + indispensability to perform the agreement +
+
+ (Article 6 (1) (b) of the GDPR) +
+ + analyze of data provided + + + your consent +
+
+ (Article 6 (1) (a) of the GDPR) +
+ + Mudita's website and forum users + + + + necessary cookies + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+ + marketing, functional, analytical cookies or our partners cookies + + + your consent +
+
+ (Article 6 (1) (a) of the GDPR) +
+ + transferring marketing and promotional information including our + partners + + + your consent +
+
+ (Article 6 (1) (a) of the GDPR) +
+ + performance of the agreement concluded with us (forum users) + + + indispensability to perform the agreement +
+
+ (Article 6 (1) (b) of the GDPR) +
+ + sales + + + + leading to the conclusion of the agreement with us + + + indispensability to perform the agreement +
+
+ (Article 6 (1) (b) of the GDPR) +
+ + performance of the agreement concluded with us and handling complaints + concerning the agreements concluded with us + + + indispensability to perform the agreement +
+
+ (Article 6 (1) (b) of the GDPR) +
+ + performance of additional services + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+ + transferring marketing and promotional information including our + partners + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+
or your consent +
+
+ (Article 6 (1) (a) of the GDPR) +
+ + questions for us + + + + responding to the questions sent via: contact form, forum, email or + phone number + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+ + general + + + + analytical purposes (e.g. selection of the services to the needs of our + clients, optimization of our products / services based on your comments + on this topic, optimalization of the service processes based on the + process of sales service and after-sales service, including complaint, + clients' satisfaction survey and determining of the quality of our + service) + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+ + possible establishment, investigation or defence against claims (i.e. + evidence purposes) + + + our legitimate interest +
+
+ (Article 6 (1) (f) of the GDPR) +
+ + storage of accounting documents + + + our obligation to keep accounting documents under tax law +
+
+ (Article 6 (1) (c) of the GDPR in conjunction with Article 86 § 1 of the + Tax Ordinance Act) +
+
+ + Who has access to your personal data? + - 3. Where do we obtain your personal data from? Most of the data we - receive directly from you. You provide us this data: a. By sending an - e-mail or by contacting us through our profile on social networks, e.g. - Facebook, Twitter, etc. b. During a conversation with the Mudita Staff c. - By agreement for sending us data about errors that may occur while using - Mudita Pure, Mudita Harmony, and Center. - - - 4. What is the scope of data processed? We process your personal - database exclusively on your consent (basis of Art. 6 sec. 1 letter. a of - the GDPR). With your permission, Mudita will gain access to information - concerning the following errors (general errors, crash dumps, warnings, - hard faults; Bluetooth data - state, signal power, controls state; VoLTE - - network mode, on/off settings, phone call state; power management - - average battery voltage level, minimal and maximal voltage, average - current from the battery, state of charge; cellular - SIM slot selected, - Mobile Network Code and Mobile Country Code) that may occur while using - Mudita Pure and the Software. The aim of accessing such information is to - fix errors and further develop the Mudita Pure device and the Software. - Information accessed this way will be limited to diagnostic data, - including the description of the error, type of operating system, version - of the Software, and other technical data, as well as data containing the - IP address of the computer that was used to check for updates for the - Software or the Mudita Pure device. No other data will be accessed by - Mudita in connection with your use of the Software. - - - 5. How long do we process your personal data? Until you revoke your - consent, but no longer than 3 years. - - - 6. Who is the recipient of your personal data? We do not sell your - data. We may share your personal data with our employees and associates if - it is related to the aim described in point 4 above. Mudita holds all - copyrights and licenses for the Software. Certain elements of the Software - use or contain software provided by third parties as well as other - copyrighted material, which you are entitled to use as part of the - Software in accordance with these Terms. The Software may also use certain - services provided by third parties. However, before accessing any service, - you will be asked to give your permission and accept the terms defined by - the service’s provider. + 4. We may share your personal data with the following categories of + entities: + + a) employees and associates, + + + b) related undertakings and cooperating entities, including our partners, + + + c) entities supporting our activity, including but not limited to legal, + accounting, IT, logistics, marketing terms, etc. + + + How long is your personal data stored? + - 7. How do we process your personal data? We process personal data - in accordance with applicable law, in particular in accordance with GDPR. - We have the following rules in mind when we process your personal - information: + 5. Your personal data is processed: - a. Adequacy rule. We process only data that is necessary to achieve - a given processing goal. We have carried out an analysis of the - fulfillment of this rule for each business process; + a) in relation to conclusion and performance of the agreement or providing + other services (using Mudita’s website/forum, software users, products + sales, necessary cookies) - for the time necessary to perform the + contract; - b. Transparency rule. You should have full knowledge of what is - happening with your data. This document, in which we try to provide you - with complete information about the rules of processing your personal data - by us, is its manifestation; + b) provided under your consent (cookies files or marketing data) - unless + you withdraw your consent or further processing will be pointless; + contract; - c. Accuracy rule. We strive to keep your personal data in our - systems up-to-date and truthful. If you find that in some area your - personal data have not been updated or are incorrect, please contact us at - the email address{" "} - office@mudita.com; + c) related to answering to your inquiries - for the time necessary to + perform the obligation and for the time necessary to achieve our goals; - d. Integrity and confidentiality rule. We apply the necessary - measures to safeguard the confidentiality and integrity of your personal - data. We are constantly improving them, along with the changing - environment and technological progress. Security includes physical and - technological measures restricting access to your data, as well as - appropriate measures to prevent loss of your data; + d) for evidence purposes to establish the existence of claims, their + pursuit or defence against them - until the end of the limitation period + for possible claims in this respect (this period is determined by the + provisions of the Polish Civil Code), and in the case of its use in public + legal proceedings until the time when after their final termination, + extraordinary appeal measures are not be applicable any more (such period + shall be determined by the provisions of the Polish Code of Civil + Procedure); - e. Accountability rule. We want to be able to account for each of - our actions regarding personal data so that in the event of your inquiry - we can give you full and reliable information about what actions we have - been carried out on your data. + e) in connection with the storage of accounting documentation - until the + expiry of the limitation period for the tax obligation related to the + relevant transaction (this period is determined by the provisions of the + Tax Ordinance Act); + + What rights do you have in relation to the processing of your personal + data? + - 8.{" "} - - What rights do you have regarding the processing of your personal data? - {" "} - The provisions of law give you a number of rights that you can use at any - time. Unless you abuse these rights (e.g. unreasonable daily requests for - information), exercising them will be free of charge and should be easy to - implement. Your rights include: + 6. You shall have the right: - a. The right to access your personal data. This right means that - you can ask us to export from our databases the information we have about - you and send it to you in one of the commonly used formats (e.g. XLSX, - DOCX, etc.); + a) to access to your data and receive a copy of it; + + + b) to rectify (correct) your data; - b. The right to correct personal data. If you find out that the - personal data we process is incorrect, you may ask us to correct it and we - will be obliged to do so. In this case, we have the right to ask you for a - document or proof of the change; + c) to delete data: if, in your opinion, there are no grounds for us to + process your data, you can request us to delete it; - c. The right to seek restriction of personal data processing. If - despite the fact that we adhere to the adequacy principle, that is we - process only data that is necessary to achieve a given processing goal, - you consider that for a specific purpose we process too wide a catalog of - your personal data, you have the right to request that we restrict (limit) - the scope of processing. If the request does not oppose the requirements - imposed on us by applicable law, or it is not necessary for the - performance of the contract, we will accept your request; + d) to limit data processing: you can request that we limit the processing + of your personal data only to their storage or performance of the + activities agreed with you, if in your opinion we have incorrect data + about you or we process it unreasonably; or you do not want us to delete + it because you need it to establish, pursue or defend claims; or for the + duration of your objection to data processing; - d. The right to request the erasure of personal data. This right, - also known as the right to be forgotten, means that you can demand that we - remove any information that contains your personal information from our - systems and any other records. Remember, however, that we will not be able - to do so if we are obliged to process your data under provisions of law - (for example transaction documents for tax purposes, obligation to ensure - the accountability of our activities). In each case, however, we will - remove your personal data to the fullest extent possible, and where it is - not possible, we will ensure their pseudonymization (which means that the - data subject cannot be identified without a corresponding key). Allowing - this, your data we need to keep in line with applicable law will be - available only to a very limited group of people in our organization; + e) to object to the processing of data: objection due to special situation + - you shall have the right to object to the processing of your data on the + basis of a legitimate interest for purposes other than direct marketing, + as well as when the processing is necessary for us to fulfil a task + carried out in the public interest or the exercising public authority + entrusted to us, then you should indicate your special situation, which, + in your opinion, justifies the our discontinuation of the processing + covered by the objection, we will stop processing your data for such + purposes, unless we demonstrate that the grounds for processing your data + override your rights or that your data is necessary for us to establish, + pursue or defend claims; - e. The right to personal data portability. In accordance with the - GDPR, you can ask us to port the data you provided to us in the course of - all our contacts and all cooperation to a separate file, for the purpose - of further transfer to another data controller; + f) to transfer data; - f. The right to withdraw consent. If we process your personal data - on the basis of your consent, you can revoke this consent at any time. - Withdrawal of your consent will not affect the lawfulness of the - processing previously performed on the basis of the consent (prior to its - withdrawal). However, we would like to inform you that your personal data - in the scope of the purpose covered by the revoked consent will cease to - be processed for this purpose only. Your personal data subject to consent - will be further processed in order to fulfill our obligations under the - law, including, in particular, the obligation to account for the - correctness of personal data processing, or for the purposes based on our - legitimate interest. + g) to lodge a complaint with the supervisory authority: if you think that + we process your data unlawfully, you can lodge a complaint to the + supervisory authority responsible for overseeing compliance with the + provisions on the protection of personal data (the President of the Office + for Personal Data Protection); + + h) to withdraw your consent to the processing of personal data: at any + time you shall have the right to withdraw your consent to the processing + of your personal data, which we process on the basis of your consent, the + withdrawal of consent will not influence the legal compliance of the + processing which was performed on the basis of your consent before its + withdrawal. + + + How to exercise your personal data rights? + + + 7. In order to exercise your rights, send a request to the contact details + indicated in clause 1. Before exercising your rights you shall remember + that we will have to make sure it is you, that is, to appropriately + identify you. + + + Is providing personal data mandatory? + - Unless you abuse the rights listed above (e.g. unjustified daily requests - for information), using them will be free of charge and should be easy to - implement. + 8. Concluding the agreement with us is voluntary. However, providing + personal data in connection with the agreement is a condition for its + conclusion and then performance - without providing your personal data, it + is not possible to conclude the agreement with us. + Cookies - You can perform the above-mentioned rights by contacting us at the e-mail - address office@mudita.com or by - post on Mudita sp. z o.o., Jana Czeczota Street No.6, 02-607 - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - Warsaw, with the note "Personal data - {/* AUTO DISABLED - fix me if you like :) */} - {/* eslint-disable-next-line react/no-unescaped-entities */} - ". + 9. Cookies are tiny text files that are downloaded to your computer, to + improve your experience during using our website. They serve also many + functions. They are very important for the proper operation of most + websites, including those where we log in to our account. These files + identify the computer and the user, they are not malicious programs or + associated with any private data. - In all matters related to personal data, you can always write to us, - especially when any action or situation you encounter raises your concerns - about its legality or if you feel that your rights or freedoms may be - violated. In this case, we will answer your questions and concerns and - immediately address the issue. + We use cookies to improve your experience while you navigate through the + our websites accordingly to our cookies policy. Out of these cookies, the + cookies that are categorized as necessary are stored on your browser as + they as essential for the working of basic functionalities of the our + websites. - If you believe that in any way we have violated the rules for the - processing of your personal data, you have the right to submit a complaint - directly to the supervisory authority ( - - from 22 June 2021, it is the President of the Office for Personal Data - Protection in Poland - - ). As part of exercising this right, you should provide a full description - of the situation and indicate what action you consider as violating your - rights or freedoms. The complaint should be submitted directly to the - supervisory authority. + We also use marketing, functional, analytical or third-party cookies that + help us analyze and understand how you use our websites, to store user + preferences and provide them with content and advertisements that are + relevant to you. These cookies will only be stored on your browser with + your consent to do so. You also have the option to opt-out of these + cookies. But opting out of some of these cookies may have an effect on + your browsing experience. - 9. Is it your obligation to provide your data? You provide your - personal data voluntarily. There is no provision that would impose a legal - obligation on you to provide it. + You can express your consent or objection to the use of cookies after + entering our website. Before granting your consent, you can read the full + list and details about cookies that we use on our website. - However, if you want to use our services, you must provide data that will - allow us to conclude the contract with you, to perform it, to fulfill our - legal obligations regarding due tax settlement, and to prepare - documentation for the purposes of accountability of our activities. + Details about settings the rules for the use of cookies, including + disabling cookies, by the browser are available at the links below: + + + Additional information + - Personal data provided for contact or marketing purposes is necessary for - us to allow us to contact you or to carry out marketing activities that - you agree to, or at least you do not oppose them. If we will be not - provided with it, our communication with you will be either difficult - (e.g. if you provide only your telephone number, but no e-mail address), - or even impossible (if no contact details will be provided). + 10. Controller does not share and has no intention to share client's + personal data with third country or international organisation. Only + except may be the United States (based on standard contractual clauses + according to Commission Implementing Decision (EU) 2021/914 of 4 June 2021 + on standard contractual clauses for the transfer of personal data to third + countries pursuant to Regulation (EU) 2016/679 of the European Parliament + and of the Council), which results from the fact, that personal data may + be uploaded to the servers of applications, software and IT services + providers, located in the United States. ) diff --git a/packages/app/src/settings/components/settings/index.ts b/packages/app/src/settings/components/settings/index.ts index 3f8ea9dbdc..4a52ad2530 100644 --- a/packages/app/src/settings/components/settings/index.ts +++ b/packages/app/src/settings/components/settings/index.ts @@ -3,6 +3,4 @@ * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md */ -export * from "./settings-ui.component" -export * from "./settings.component" export * from "./settings-ui.styled" diff --git a/packages/app/src/settings/components/settings/settings-ui.component.tsx b/packages/app/src/settings/components/settings/settings-ui.component.tsx deleted file mode 100644 index 118594a041..0000000000 --- a/packages/app/src/settings/components/settings/settings-ui.component.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import React from "react" -import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { ActionsWrapper } from "App/__deprecated__/renderer/components/rest/messages/threads-table.component" -import { TextDisplayStyle } from "App/__deprecated__/renderer/components/core/text/text.component" -import { FormattedMessage } from "react-intl" -import { SettingsToggler } from "App/settings/components/settings-toggler/settings-toggler.component" -import { SettingsTestIds } from "App/settings/components/settings/settings.enum" -import { defineMessages } from "react-intl" -import { Feature, flags } from "App/feature-flags" -import { - SettingsWrapper, - SettingsTableRow, - Data, - SettingsLabel, - SettingsTooltip, -} from "App/settings/components/settings/settings-ui.styled" -import { Properties } from "App/settings/components/settings/settings-ui.interface" - -const messages = defineMessages({ - tooltipDescription: { id: "module.settings.collectingDataTooltip" }, -}) - -export const SettingsUI: FunctionComponent = ({ - tethering, - toggleTethering, - collectingData, - toggleCollectionData, -}) => { - return ( - - {/*TODO: Remove condition below when tethering will be available on phone*/} - {flags.get(Feature.TetheringEnabled) && ( - - - - - - - - - - - )} - - - - - - - - - - - - - ) -} diff --git a/packages/app/src/settings/components/settings/settings-ui.interface.ts b/packages/app/src/settings/components/settings/settings-ui.interface.ts deleted file mode 100644 index 3fde7e2e9f..0000000000 --- a/packages/app/src/settings/components/settings/settings-ui.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -export interface Properties { - autostart: boolean - tethering: boolean - collectingData: boolean | undefined - toggleTethering: (value: boolean) => void - toggleCollectionData: (value: boolean) => void -} diff --git a/packages/app/src/settings/components/settings/settings.component.tsx b/packages/app/src/settings/components/settings/settings.component.tsx deleted file mode 100644 index 3c84db2b05..0000000000 --- a/packages/app/src/settings/components/settings/settings.component.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import React, { ComponentProps } from "react" -import { FunctionComponent } from "App/__deprecated__/renderer/types/function-component.interface" -import { SettingsUI } from "App/settings/components/settings/settings-ui.component" - -type Properties = ComponentProps - -export const Settings: FunctionComponent = ({ - ...uiComponentProperties -}) => { - return -} diff --git a/packages/app/src/settings/components/settings/settings.enum.ts b/packages/app/src/settings/components/settings/settings.enum.ts deleted file mode 100644 index a747877101..0000000000 --- a/packages/app/src/settings/components/settings/settings.enum.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -export enum SettingsTestIds { - Wrapper = "settings-wrapper", - Description = "settings-description", - TableRow = "settings-tablerow", - TogglerActive = "toggler-active", - TogglerInactive = "toggler-inactive", -} diff --git a/packages/app/src/settings/components/settings/settings.stories.tsx b/packages/app/src/settings/components/settings/settings.stories.tsx deleted file mode 100644 index c95ab6ac29..0000000000 --- a/packages/app/src/settings/components/settings/settings.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { storiesOf } from "@storybook/react" -import React from "react" -import { SettingsUI } from "App/settings/components/settings/settings-ui.component" - -storiesOf("Settings/Settings(connection)", module).add( - "Settings(connection)", - () => ( -
- -
- ) -) diff --git a/packages/app/src/settings/components/settings/settings.test.tsx b/packages/app/src/settings/components/settings/settings.test.tsx deleted file mode 100644 index 44d4aa8af5..0000000000 --- a/packages/app/src/settings/components/settings/settings.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { renderWithThemeAndIntl } from "App/__deprecated__/renderer/utils/render-with-theme-and-intl" -import "@testing-library/jest-dom/extend-expect" -import React from "react" -import { SettingsUI } from "App/settings/components/settings/settings-ui.component" -import { SettingsTestIds } from "App/settings/components/settings/settings.enum" - -const renderer = ( - config = { - autostart: false, - tethering: false, - collectingData: false, - toggleTethering: jest.fn(), - toggleCollectionData: jest.fn(), - } -) => renderWithThemeAndIntl() - -test("renders wrapper properly", () => { - const { queryByTestId } = renderer() - expect(queryByTestId(SettingsTestIds.Wrapper)).toBeInTheDocument() -}) - -test("renders at least one table row", () => { - const { queryAllByTestId } = renderer() - expect( - queryAllByTestId(SettingsTestIds.TableRow).length - ).toBeGreaterThanOrEqual(1) -}) diff --git a/packages/app/src/settings/constants/event.constant.ts b/packages/app/src/settings/constants/event.constant.ts index 4d40067552..f3ce2dab41 100644 --- a/packages/app/src/settings/constants/event.constant.ts +++ b/packages/app/src/settings/constants/event.constant.ts @@ -19,6 +19,9 @@ export enum SettingsEvent { ToggleTethering = "TOGGLE_TETHERING", ToggleUpdateAvailable = "TOGGLE_UPDATE_AVAILABLE", ToggleCollectionData = "TOGGLE_COLLECTION_DATA", + TogglePrivacyPolicyAccepted = "TOGGLE_PRIVACY_POLICY_ACCEPTED", CheckUpdateAvailable = "CHECK_UPDATE_AVAILABLE", SendDiagnosticData = "SEND_DIAGNOSTIC_DATA", + DeleteCollectingData = "DELETE_COLLECTING_DATA", + SetCheckingForUpdate = "SET_CHECKING_FOR_UPDATE", } diff --git a/packages/app/src/settings/controllers/settings.controller.test.ts b/packages/app/src/settings/controllers/settings.controller.test.ts index 6cc5d8ee99..2c6fc15802 100644 --- a/packages/app/src/settings/controllers/settings.controller.test.ts +++ b/packages/app/src/settings/controllers/settings.controller.test.ts @@ -25,6 +25,7 @@ export const fakeSettings: Settings = { language: "en-US", neverConnected: true, collectingData: false, + privacyPolicyAccepted: false, diagnosticSentTimestamp: 0, ignoredCrashDumps: [], } diff --git a/packages/app/src/settings/dto/settings.object.ts b/packages/app/src/settings/dto/settings.object.ts index c4afb7d7b6..1586d361e2 100644 --- a/packages/app/src/settings/dto/settings.object.ts +++ b/packages/app/src/settings/dto/settings.object.ts @@ -15,6 +15,7 @@ export interface Settings { ignoredCrashDumps: string[] diagnosticSentTimestamp: number collectingData: boolean | undefined + privacyPolicyAccepted: boolean | undefined neverConnected: boolean tray: boolean nonStandardAudioFilesConversion: boolean diff --git a/packages/app/src/settings/hooks/use-online-checker.ts b/packages/app/src/settings/hooks/use-online-checker.ts new file mode 100644 index 0000000000..3c99981ea1 --- /dev/null +++ b/packages/app/src/settings/hooks/use-online-checker.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import { useState, useEffect } from "react" + +export const useOnlineChecker = (): boolean => { + const [online, setOnline] = useState(window.navigator.onLine) + + useEffect(() => { + const intervalId = setInterval(() => { + if (window.navigator.onLine !== online) { + setOnline(window.navigator.onLine) + } + }, 500) + return () => clearInterval(intervalId) + }, [online, setOnline]) + + return online +} diff --git a/packages/app/src/settings/reducers/settings.interface.ts b/packages/app/src/settings/reducers/settings.interface.ts index 9d92458d46..9087af1260 100644 --- a/packages/app/src/settings/reducers/settings.interface.ts +++ b/packages/app/src/settings/reducers/settings.interface.ts @@ -19,4 +19,5 @@ export interface SettingsState extends Settings { loading: boolean updateRequired: boolean updateAvailable: boolean | undefined + checkingForUpdate: boolean } diff --git a/packages/app/src/settings/reducers/settings.reducer.test.ts b/packages/app/src/settings/reducers/settings.reducer.test.ts index 732e74e1f0..763e40fbf6 100644 --- a/packages/app/src/settings/reducers/settings.reducer.test.ts +++ b/packages/app/src/settings/reducers/settings.reducer.test.ts @@ -35,6 +35,7 @@ const settings: Omit< language: "en-US", neverConnected: true, collectingData: false, + privacyPolicyAccepted: false, diagnosticSentTimestamp: 0, ignoredCrashDumps: [], updateRequired: false, @@ -44,8 +45,10 @@ const settings: Omit< lowestSupportedProductVersion: { MuditaHarmony: "1.5.0", MuditaPure: "1.0.0", + MuditaKompakt: "2.0.0", }, }, + checkingForUpdate: false, } test("empty event returns initial state", () => { diff --git a/packages/app/src/settings/reducers/settings.reducer.ts b/packages/app/src/settings/reducers/settings.reducer.ts index 1cae7379b1..c6ae5a17c6 100644 --- a/packages/app/src/settings/reducers/settings.reducer.ts +++ b/packages/app/src/settings/reducers/settings.reducer.ts @@ -18,11 +18,14 @@ import { toggleTethering, toggleApplicationUpdateAvailable, toggleCollectionData, + togglePrivacyPolicyAccepted, setConversionFormat, setConvert, setNonStandardAudioFilesConversion, setLowBattery, + setCheckingForUpdate, } from "App/settings/actions" +import { deleteCollectingData } from "App/settings/actions/delete-collecting-data.action" export const initialState: SettingsState = { applicationId: "", @@ -37,6 +40,7 @@ export const initialState: SettingsState = { ignoredCrashDumps: [], diagnosticSentTimestamp: 0, collectingData: false, + privacyPolicyAccepted: undefined, neverConnected: false, tray: false, nonStandardAudioFilesConversion: false, @@ -50,6 +54,7 @@ export const initialState: SettingsState = { updateAvailable: undefined, loaded: false, loading: false, + checkingForUpdate: false, } export const settingsReducer = createReducer( @@ -86,6 +91,8 @@ export const settingsReducer = createReducer( state.updateRequired = action.payload.updateRequired state.lowestSupportedVersions = action.payload.lowestSupportedVersions state.currentVersion = action.payload.currentVersion + state.privacyPolicyAccepted = action.payload.privacyPolicyAccepted + state.checkingForUpdate = action.payload.checkingForUpdate }) .addCase(setLatestVersion, (state, action) => { @@ -104,6 +111,12 @@ export const settingsReducer = createReducer( state.collectingData = action.payload }) + .addCase(togglePrivacyPolicyAccepted.fulfilled, (state, action) => { + state.privacyPolicyAccepted = action.payload + }) + .addCase(deleteCollectingData.fulfilled, (state, _) => { + state.collectingData = undefined + }) .addCase(setDiagnosticTimestamp.fulfilled, (state, action) => { state.diagnosticSentTimestamp = action.payload }) @@ -142,5 +155,9 @@ export const settingsReducer = createReducer( .addCase(setIncomingCalls.fulfilled, (state, action) => { state.incomingCalls = action.payload }) + + .addCase(setCheckingForUpdate, (state, action) => { + state.checkingForUpdate = action.payload + }) } ) diff --git a/packages/app/src/settings/selectors/get-device-lowest-version.selector.test.ts b/packages/app/src/settings/selectors/get-device-lowest-version.selector.test.ts index bebe3a8442..331653b34b 100644 --- a/packages/app/src/settings/selectors/get-device-lowest-version.selector.test.ts +++ b/packages/app/src/settings/selectors/get-device-lowest-version.selector.test.ts @@ -71,6 +71,7 @@ describe("When `deviceType` and `lowestSupportedVersions` has been provided", () lowestSupportedProductVersion: { MuditaHarmony: "1.0.0", MuditaPure: "2.0.0", + MuditaKompakt: "3.0.0", }, }, }, @@ -90,6 +91,7 @@ describe("When `deviceType` and `lowestSupportedVersions` has been provided", () lowestSupportedProductVersion: { MuditaHarmony: "1.0.0", MuditaPure: "2.0.0", + MuditaKompakt: "3.0.0", }, }, }, diff --git a/packages/app/src/settings/services/configuration.service.test.ts b/packages/app/src/settings/services/configuration.service.test.ts index 558066208a..435772ec69 100644 --- a/packages/app/src/settings/services/configuration.service.test.ts +++ b/packages/app/src/settings/services/configuration.service.test.ts @@ -38,16 +38,21 @@ const configuration: Configuration = { productVersions: { MuditaHarmony: "1.1.1", MuditaPure: "2.2.2", + MuditaKompakt: "3.3.3", }, } - -jest.mock("App/settings/static/default-app-configuration.json", () => ({ +const defaultConfig = { centerVersion: "1.0.0-default", productVersions: { MuditaHarmony: "1.1.1-default", MuditaPure: "2.2.2-default", }, -})) +} + +jest.mock( + "App/settings/static/default-app-configuration.json", + () => defaultConfig +) describe("When API return success status code", () => { test("returns API response", async () => { @@ -66,15 +71,15 @@ describe("When API return failed status code", () => { axiosMock.onGet("http://localhost/v2-app-configuration").replyOnce(500, { error: "Luke, I'm your error", }) - + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const appConfiguration = require("../static/app-configuration.json") const subject = new ConfigurationService() const result = await subject.getConfiguration() - expect(result).toEqual({ - centerVersion: "1.0.0-default", - productVersions: { - MuditaHarmony: "1.1.1-default", - MuditaPure: "2.2.2-default", - }, - }) + if (appConfiguration) { + expect(result).toEqual(appConfiguration) + } else { + expect(result).toEqual(defaultConfig) + } }) }) diff --git a/packages/app/src/settings/services/configuration.service.ts b/packages/app/src/settings/services/configuration.service.ts index 8c7c734ea6..347f0f1e61 100644 --- a/packages/app/src/settings/services/configuration.service.ts +++ b/packages/app/src/settings/services/configuration.service.ts @@ -44,7 +44,7 @@ export class ConfigurationService { try { // AUTO DISABLED - fix me if you like :) // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.defaultConfiguration = require("../app-configuration.json") + this.defaultConfiguration = require("../static/app-configuration.json") } catch { console.error("read app-configuration.json is failed") } diff --git a/packages/app/src/settings/services/settings.service.test.ts b/packages/app/src/settings/services/settings.service.test.ts index 2b76ff411a..15526b9c55 100644 --- a/packages/app/src/settings/services/settings.service.test.ts +++ b/packages/app/src/settings/services/settings.service.test.ts @@ -33,6 +33,7 @@ export const fakeSettings: Settings = { language: "en-US", neverConnected: true, collectingData: false, + privacyPolicyAccepted: false, diagnosticSentTimestamp: 0, ignoredCrashDumps: [], } diff --git a/packages/app/src/settings/services/settings.service.ts b/packages/app/src/settings/services/settings.service.ts index d761fef457..b8052c992f 100644 --- a/packages/app/src/settings/services/settings.service.ts +++ b/packages/app/src/settings/services/settings.service.ts @@ -32,7 +32,11 @@ export class SettingsService { } updateSettings({ key, value }: SettingsUpdateOption): SettingsValue { - this.store.set(key, value) + if (value !== undefined) { + this.store.set(key, value) + } else { + this.store.delete(key) + } return this.store.get(key) } diff --git a/packages/app/src/settings/settings.container.tsx b/packages/app/src/settings/settings.container.tsx deleted file mode 100644 index 4de65537cf..0000000000 --- a/packages/app/src/settings/settings.container.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) Mudita sp. z o.o. All rights reserved. - * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md - */ - -import { connect } from "react-redux" -import { Settings } from "App/settings/components" -import { ReduxRootState } from "App/__deprecated__/renderer/store" -import { toggleCollectionData, toggleTethering } from "App/settings/actions" - -const mapStateToProps = (state: ReduxRootState) => ({ - autostart: state.settings.autostart, - tethering: state.settings.tethering, - collectingData: state.settings.collectingData, -}) - -const mapDispatchToProps = { - toggleTethering, - toggleCollectionData, -} - -export default connect(mapStateToProps, mapDispatchToProps)(Settings) diff --git a/packages/app/src/settings/static/default-app-configuration.json b/packages/app/src/settings/static/default-app-configuration.json index 0a63df04b4..4f2062a634 100644 --- a/packages/app/src/settings/static/default-app-configuration.json +++ b/packages/app/src/settings/static/default-app-configuration.json @@ -1,5 +1,5 @@ { - "centerVersion": "1.7.0", + "centerVersion": "2.0.2", "productVersions": { "MuditaPure": "1.6.0", "MuditaHarmony": "2.0.0" diff --git a/packages/app/src/settings/store/migrations/002-privacy-policy-accepted.migration.ts b/packages/app/src/settings/store/migrations/002-privacy-policy-accepted.migration.ts new file mode 100644 index 0000000000..240e6a577e --- /dev/null +++ b/packages/app/src/settings/store/migrations/002-privacy-policy-accepted.migration.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +// AUTO DISABLED - fix me if you like :) +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +export const privacyPolicyAcceptedMigration = (store: any) => { + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + store.set("privacyPolicyAccepted", false) +} diff --git a/packages/app/src/settings/store/migrations/003-os-download-location.migration.ts b/packages/app/src/settings/store/migrations/003-os-download-location.migration.ts new file mode 100644 index 0000000000..37fe637e25 --- /dev/null +++ b/packages/app/src/settings/store/migrations/003-os-download-location.migration.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) Mudita sp. z o.o. All rights reserved. + * For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md + */ + +import getAppPath from "App/__deprecated__/main/utils/get-app-path" +import path from "path" + +// AUTO DISABLED - fix me if you like :) +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +export const osDownloadLocationMigration = (store: any) => { + // AUTO DISABLED - fix me if you like :) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + store.set("osDownloadLocation", path.join(getAppPath(), "os", "downloads")) +} diff --git a/packages/app/src/settings/store/migrations/index.ts b/packages/app/src/settings/store/migrations/index.ts index 2b9c6b9a69..31f0b8844e 100644 --- a/packages/app/src/settings/store/migrations/index.ts +++ b/packages/app/src/settings/store/migrations/index.ts @@ -4,3 +4,5 @@ */ export * from "./001-remove-unused-fields.migration" +export * from "./002-privacy-policy-accepted.migration" +export * from "./003-os-download-location.migration" diff --git a/packages/app/src/settings/store/schemas/settings.schema.ts b/packages/app/src/settings/store/schemas/settings.schema.ts index 4a5c155b1c..3637b09052 100644 --- a/packages/app/src/settings/store/schemas/settings.schema.ts +++ b/packages/app/src/settings/store/schemas/settings.schema.ts @@ -84,7 +84,7 @@ export const settingsSchema: Schema = { }, osDownloadLocation: { type: "string", - default: path.join(getAppPath(), "pure", "os", "downloads"), + default: path.join(getAppPath(), "os", "downloads"), }, language: { type: "string", @@ -98,6 +98,10 @@ export const settingsSchema: Schema = { type: "boolean", default: undefined, }, + privacyPolicyAccepted: { + type: "boolean", + default: false, + }, diagnosticSentTimestamp: { type: "number", default: 0, diff --git a/packages/app/src/settings/store/settings.store.ts b/packages/app/src/settings/store/settings.store.ts index 011777d168..cb9194331c 100644 --- a/packages/app/src/settings/store/settings.store.ts +++ b/packages/app/src/settings/store/settings.store.ts @@ -8,7 +8,11 @@ import { settingsSchema } from "App/settings/store/schemas" import project from "../../../package.json" import getAppPath from "App/__deprecated__/main/utils/get-app-path" -import { removeUnusedFields } from "App/settings/store/migrations" +import { + privacyPolicyAcceptedMigration, + removeUnusedFields, + osDownloadLocationMigration, +} from "App/settings/store/migrations" export const settingsStore = new Store({ name: "settings", @@ -21,5 +25,7 @@ export const settingsStore = new Store({ clearInvalidConfig: process.env.NODE_ENV === "production", migrations: { ">=1.4.1": removeUnusedFields, + ">=2.0.2": privacyPolicyAcceptedMigration, + ">=2.1.0": osDownloadLocationMigration, }, }) diff --git a/packages/app/src/templates/components/template-form/byte-length.validator.ts b/packages/app/src/templates/components/template-form/byte-length.validator.ts index 4a84c1b48c..c8e846eeee 100644 --- a/packages/app/src/templates/components/template-form/byte-length.validator.ts +++ b/packages/app/src/templates/components/template-form/byte-length.validator.ts @@ -6,12 +6,15 @@ import { RegisterOptions } from "react-hook-form/dist/types" import { intl } from "App/__deprecated__/renderer/utils/intl" -export const byteLengthValidator = (): RegisterOptions => ({ +export const templateValidator = (): RegisterOptions => ({ required: intl.formatMessage({ id: "module.templates.required" }), validate: (value: string): string | undefined => { if (new Blob([value]).size > 469) { return intl.formatMessage({ id: "module.templates.tooLong" }) } + if (value.includes("\n")) { + return intl.formatMessage({ id: "module.templates.newLine" }) + } return }, }) diff --git a/packages/app/src/templates/components/template-form/template-form.component.test.tsx b/packages/app/src/templates/components/template-form/template-form.component.test.tsx index 1c505e9e26..c2bc1b064f 100644 --- a/packages/app/src/templates/components/template-form/template-form.component.test.tsx +++ b/packages/app/src/templates/components/template-form/template-form.component.test.tsx @@ -17,6 +17,7 @@ const onSaveMock = jest.fn() const textMock = "Luke, I'm your father" const longTextMock = "Duis tincidunt dui ut condimentum sagittis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris convallis neque eget mi posuere porta ut ut felis. Fusce ultrices cursus interdum. Nulla nibh eros, egestas eu felis vel, posuere pretium elit. Nullam vitae luctus eros, eu lobortis augue. Vestibulum in felis pharetra, lacinia nibh vitae, vehicula velit. Quisque massa est, placerat sed urna vitae, eleifend blandit sapien. Donec a efficitur nunc, et euismod tellus." +const textWithNewLines = "aaaaa\naaaaaaaaaaaa\n\naaaaaaaaa" const render = (props: TemplateFormProps) => { return renderWithThemeAndIntl() @@ -175,6 +176,29 @@ describe("`TemplateForm` component", () => { }) }) + test("`onSave` triggers error if text contains new line character", async () => { + const { getByTestId, queryByText } = await renderWithWaitForm({ + onClose: onCloseMock, + onSave: onSaveMock, + error: null, + template: undefined, + saving: false, + }) + const textField = getByTestId(TemplateFormTestIds.TextFiled) + + expect( + queryByText("[value] module.templates.newLine") + ).not.toBeInTheDocument() + fireEvent.change(textField, { + target: { value: textWithNewLines }, + }) + await waitFor(() => { + expect( + queryByText("[value] module.templates.newLine") + ).toBeInTheDocument() + }) + }) + test("show spinner if `saving` prop have been provided", async () => { const { getByTestId } = await renderWithWaitForm({ onClose: onCloseMock, diff --git a/packages/app/src/templates/components/template-form/template-form.component.tsx b/packages/app/src/templates/components/template-form/template-form.component.tsx index a829a39094..b0a8cdb85d 100644 --- a/packages/app/src/templates/components/template-form/template-form.component.tsx +++ b/packages/app/src/templates/components/template-form/template-form.component.tsx @@ -27,7 +27,7 @@ import { TextArea, } from "App/templates/components/template-form/template-form.styled" import { TemplateFormTestIds } from "App/templates/components/template-form/template-form-ids.enum" -import { byteLengthValidator } from "App/templates/components/template-form/byte-length.validator" +import { templateValidator } from "App/templates/components/template-form/byte-length.validator" const messages = defineMessages({ editTitle: { id: "module.templates.editTitle" }, @@ -80,6 +80,12 @@ export const TemplateForm: FunctionComponent = ({ onSave(data) }) + const onKeyDownIgnoreEnter = (event: React.KeyboardEvent) => { + if (event.key === "Enter") { + event.preventDefault() + } + } + return ( = ({