/* * Wire * Copyright (C) 2022 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import {useContext, useEffect, useState} from 'react'; import {amplify} from 'amplify'; import {container} from 'tsyringe'; import {Button, ButtonVariant, FlexBox} from '@wireapp/react-ui-kit'; import {WebAppEvents} from '@wireapp/webapp-events'; import {LoadingBar} from 'Components/LoadingBar/LoadingBar'; import {PrimaryModal} from 'Components/Modals/PrimaryModal'; import {ClientState} from 'src/script/client/ClientState'; import {User} from 'src/script/entity/User'; import {ContentState} from 'src/script/page/useAppState'; import {EventName} from 'src/script/tracking/EventName'; import {Segmentation} from 'src/script/tracking/Segmentation'; import {t} from 'Util/LocalizerUtil'; import {getLogger} from 'Util/Logger'; import {getCurrentDate} from 'Util/TimeUtil'; import {downloadBlob} from 'Util/util'; import {CancelError} from '../../backup/Error'; import {Config} from '../../Config'; import {RootContext} from '../../page/RootProvider'; enum ExportState { COMPRESSING = 'ExportState.STATE.COMPRESSING', DONE = 'ExportState.STATE.DONE', EXPORTING = 'ExportState.STATE.EXPORTING', PREPARING = 'ExportState.STATE.PREPARING', } export const CONFIG = { FILE_EXTENSION: 'desktop_wbu', }; interface HistoryExportProps { switchContent: (contentState: ContentState) => void; readonly user: User; readonly clientState?: ClientState; } const HistoryExport = ({switchContent, user, clientState = container.resolve(ClientState)}: HistoryExportProps) => { const logger = getLogger('HistoryExport'); const [historyState, setHistoryState] = useState(ExportState.PREPARING); const [hasError, setHasError] = useState(false); const [numberOfRecords, setNumberOfRecords] = useState(0); const [numberOfProcessedRecords, setNumberOfProcessedRecords] = useState(0); const [archiveBlob, setArchiveBlob] = useState(null); const mainViewModel = useContext(RootContext); useEffect(() => { showBackupModal(); }, []); if (!mainViewModel) { return null; } const {content: contentViewModel} = mainViewModel; const backupRepository = contentViewModel.repositories.backup; const loadingProgress = Math.floor((numberOfProcessedRecords / numberOfRecords) * 100); const isPreparing = !hasError && historyState === ExportState.PREPARING; const isExporting = !hasError && [ExportState.EXPORTING, ExportState.COMPRESSING].includes(historyState); const isDone = !hasError && historyState === ExportState.DONE; const replacements = { processed: numberOfProcessedRecords.toString(), progress: loadingProgress.toString(), total: numberOfRecords.toString(), }; const historyMessages: Partial> = { [ExportState.PREPARING]: t('backupExportProgressHeadline'), [ExportState.EXPORTING]: t('backupExportProgressSecondary', replacements), [ExportState.COMPRESSING]: t('backupExportProgressCompressing'), }; const loadingMessage = historyMessages?.[historyState] || ''; const dismissExport = () => { switchContent(ContentState.PREFERENCES_ACCOUNT); }; const onProgress = (processedNumber: number) => { setHistoryState(ExportState.EXPORTING); setNumberOfProcessedRecords(prevState => prevState + processedNumber); }; const onSuccess = (archiveBlob: Blob) => { setHistoryState(ExportState.DONE); setHasError(false); setArchiveBlob(archiveBlob); }; const onError = (error: Error) => { if (error instanceof CancelError) { logger.log('History export was cancelled'); dismissExport(); return; } setHasError(true); logger.error(`Failed to export history: ${error.message}`, error); }; const downloadArchiveFile = () => { const userName = user.username(); const fileExtension = CONFIG.FILE_EXTENSION; const sanitizedBrandName = Config.getConfig().BRAND_NAME.replace(/[^A-Za-z0-9_]/g, ''); const filename = `${sanitizedBrandName}-${userName}-Backup_${getCurrentDate()}.${fileExtension}`; dismissExport(); if (archiveBlob) { downloadBlob(archiveBlob, filename, 'application/octet-stream'); } }; const onCancel = () => { amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.HISTORY.BACKUP_CANCELLED, { [Segmentation.GENERAL.STEP]: Segmentation.BACKUP_CREATION.CANCELLATION_STEP.DURING_BACKUP, }); backupRepository.cancelAction(); }; const showBackupModal = () => { PrimaryModal.show(PrimaryModal.type.PASSWORD_ADVANCED_SECURITY, { primaryAction: { action: async (password: string, hasMultipleAttempts: boolean) => { exportHistory(password, hasMultipleAttempts); }, text: t('backupEncryptionModalAction'), }, secondaryAction: [ { action: () => { amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.HISTORY.BACKUP_CANCELLED, { [Segmentation.GENERAL.STEP]: Segmentation.BACKUP_CREATION.CANCELLATION_STEP.BEFORE_BACKUP, }); dismissExport(); }, text: t('backupEncryptionModalCloseBtn'), }, ], passwordOptional: true, text: { closeBtnLabel: t('backupEncryptionModalCloseBtn'), input: t('backupEncryptionModalPlaceholder'), message: t('backupEncryptionModalMessage'), title: t('backupEncryptionModalTitle'), }, }); }; const exportHistory = async (password: string, hasMultipleAttempts: boolean) => { setHistoryState(ExportState.PREPARING); setHasError(false); try { const startTime = Date.now(); const numberOfRecords = await backupRepository.getBackupInitData(); logger.log(`Exporting '${numberOfRecords}' records from history`); setNumberOfRecords(numberOfRecords); setNumberOfProcessedRecords(0); if (clientState.currentClient) { const archiveBlob = await backupRepository.generateHistory( user, clientState.currentClient.id, onProgress, password, ); amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.HISTORY.BACKUP_CREATED, { // converting to seconds [Segmentation.BACKUP_CREATION.CREATION_DURATION]: Math.ceil((Date.now() - startTime) / 1000), [Segmentation.BACKUP_CREATION.PASSWORD]: password ? Segmentation.GENERAL.YES : Segmentation.GENERAL.NO, [Segmentation.BACKUP_CREATION.PASSWORD_MULTIPLE_ATTEMPTS]: hasMultipleAttempts ? Segmentation.GENERAL.YES : Segmentation.GENERAL.NO, }); onSuccess(archiveBlob); logger.log(`Completed export of '${numberOfRecords}' records from history`); } else { throw new Error('No local client found'); } } catch (error) { onError(error as Error); } }; return (
{isPreparing && } {isExporting && ( <> )} {isDone && ( <>

{t('backupExportSuccessHeadline')}

{t('backupExportSuccessSecondary')}

)} {hasError && (

{t('backupExportGenericErrorHeadline')}

{t('backupExportGenericErrorSecondary')}

)}
); }; export {HistoryExport};