diff --git a/components/settings/about.js b/components/settings/about.js index 5d7cba1cc442f36fef2191ee2a4f4573e518f5c7..49f584394875d1c672c30ec8bf9ef79e28f9a4db 100644 --- a/components/settings/about.js +++ b/components/settings/about.js @@ -1,20 +1,20 @@ import React, { Component } from 'react' -import { View, ScrollView } from 'react-native' +import { ScrollView } from 'react-native' import AppText from '../app-text' +import SettingsSegment from './settings-segment' import styles from '../../styles/index' import labels from '../../i18n/en/settings' + export default class AboutSection extends Component { render() { return ( <ScrollView> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}>{`${labels.aboutSection.title} `}</AppText> + <SettingsSegment title={`${labels.aboutSection.title} `}> <AppText>{`${labels.aboutSection.segmentExplainer} `}</AppText> - </View> - <View style={[styles.settingsSegment, styles.settingsSegmentLast]}> - <AppText style={styles.settingsSegmentTitle}>{`${labels.credits.title} `}</AppText> + </SettingsSegment> + <SettingsSegment title={`${labels.credits.title} `} style={styles.settingsSegmentLast}> <AppText>{`${labels.credits.note}`}</AppText> - </View> + </SettingsSegment> </ScrollView> ) } diff --git a/components/settings/data-management/confirm-with-password.js b/components/settings/data-management/confirm-with-password.js new file mode 100644 index 0000000000000000000000000000000000000000..fee296af379aa093dc32335962560a161da1a228 --- /dev/null +++ b/components/settings/data-management/confirm-with-password.js @@ -0,0 +1,88 @@ +import React, { Component } from 'react' +import { View, Alert } from 'react-native' + +import nodejs from 'nodejs-mobile-react-native' +import { requestHash, openDb } from '../../../db' + +import PasswordField from '../password/password-field' +import SettingsButton from '../settings-button' + +import settings from '../../../i18n/en/settings' +import { shared } from '../../../i18n/en/labels' + +export default class ConfirmWithPassword extends Component { + constructor() { + super() + this.state = { + password: null + } + nodejs.channel.addListener( + 'password-check', + this.checkPassword, + this + ) + } + + componentWillUnmount() { + nodejs.channel.removeListener('password-check', this.checkPassword) + } + + resetPasswordInput = () => { + this.setState({ password: null }) + } + + + onIncorrectPassword = () => { + Alert.alert( + shared.incorrectPassword, + shared.incorrectPasswordMessage, + [{ + text: shared.cancel, + onPress: this.props.onCancel + }, { + text: shared.tryAgain, + onPress: this.resetPasswordInput + }] + ) + } + + checkPassword = async hash => { + try { + await openDb(hash) + this.props.onSuccess() + } catch (err) { + this.onIncorrectPassword() + } + } + + handlePasswordInput = (password) => { + this.setState({ password }) + } + + initPasswordCheck = () => { + requestHash('password-check', this.state.password) + } + + render() { + + const { password } = this.state + const labels = settings.passwordSettings + + return ( + <View> + <PasswordField + placeholder={labels.enterCurrent} + value={password} + onChangeText={this.handlePasswordInput} + /> + <SettingsButton + onPress={this.initPasswordCheck} + disabled={!password} + > + {settings.deleteSegment.title} + </SettingsButton> + </View> + ) + + } +} \ No newline at end of file diff --git a/components/settings/data-management/constants.js b/components/settings/data-management/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..610cd0340d2956a0b05a0257420d8c3212b5a488 --- /dev/null +++ b/components/settings/data-management/constants.js @@ -0,0 +1 @@ +export const EXPORT_FILE_NAME = 'data.csv' \ No newline at end of file diff --git a/components/settings/data-management/delete-data.js b/components/settings/data-management/delete-data.js new file mode 100644 index 0000000000000000000000000000000000000000..dd09c4a149a9d4b4dc64b95e636f00aac919b574 --- /dev/null +++ b/components/settings/data-management/delete-data.js @@ -0,0 +1,97 @@ +import React, { Component } from 'react' +import RNFS from 'react-native-fs' +import { Alert, ToastAndroid } from 'react-native' + +import { clearDb, isDbEmpty } from '../../../db' +import { hasEncryptionObservable } from '../../../local-storage' +import SettingsButton from '../settings-button' +import ConfirmWithPassword from './confirm-with-password' +import alertError from '../alert-error' + +import settings from '../../../i18n/en/settings' +import { shared as sharedLabels } from '../../../i18n/en/labels' +import { EXPORT_FILE_NAME } from './constants' + +const exportedFilePath = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}` + +export default class DeleteData extends Component { + constructor() { + super() + this.state = { + isPasswordSet: hasEncryptionObservable.value, + isConfirmingWithPassword: false + } + } + + onAlertConfirmation = () => { + if (this.state.isPasswordSet) { + this.setState({ isConfirmingWithPassword: true }) + } else { + this.deleteAppData() + } + } + + alertBeforeDeletion = async () => { + const { question, message, confirmation, errors } = settings.deleteSegment + if (isDbEmpty() && !await RNFS.exists(exportedFilePath)) { + alertError(errors.noData) + } else { + Alert.alert( + question, + message, + [{ + text: confirmation, + onPress: this.onAlertConfirmation + }, { + text: sharedLabels.cancel, + style: 'cancel', + onPress: this.cancelConfirmationWithPassword + }] + ) + } + } + + deleteExportedFile = async () => { + if (await RNFS.exists(exportedFilePath)) { + await RNFS.unlink(exportedFilePath) + } + } + + deleteAppData = async () => { + const { errors, success } = settings.deleteSegment + + try { + if (!isDbEmpty()) { + clearDb() + } + await this.deleteExportedFile() + ToastAndroid.show(success.message, ToastAndroid.LONG) + } catch (err) { + alertError(errors.couldNotDeleteFile) + } + this.cancelConfirmationWithPassword() + } + + cancelConfirmationWithPassword = () => { + this.setState({ isConfirmingWithPassword: false }) + } + + render() { + const { isConfirmingWithPassword } = this.state + + if (isConfirmingWithPassword) { + return ( + <ConfirmWithPassword + onSuccess={this.deleteAppData} + onCancel={this.cancelConfirmationWithPassword} + /> + ) + } + + return ( + <SettingsButton onPress={this.alertBeforeDeletion}> + {settings.deleteSegment.title} + </SettingsButton> + ) + } +} \ No newline at end of file diff --git a/components/settings/import-export/export-dialog.js b/components/settings/data-management/export-dialog.js similarity index 90% rename from components/settings/import-export/export-dialog.js rename to components/settings/data-management/export-dialog.js index 58301cf56042a89a49c7a0bd5ad6698c147b5163..cfc5f5d5d3e5e0580e6833298ffc310a12801f86 100644 --- a/components/settings/import-export/export-dialog.js +++ b/components/settings/data-management/export-dialog.js @@ -4,6 +4,7 @@ import { getCycleDaysSortedByDate } from '../../../db' import getDataAsCsvDataUri from '../../../lib/import-export/export-to-csv' import alertError from '../alert-error' import settings from '../../../i18n/en/settings' +import { EXPORT_FILE_NAME } from './constants' import RNFS from 'react-native-fs' export default async function exportData() { @@ -24,7 +25,7 @@ export default async function exportData() { } try { - const path = RNFS.DocumentDirectoryPath + '/data.csv' + const path = `${RNFS.DocumentDirectoryPath}/${EXPORT_FILE_NAME}` await RNFS.writeFile(path, data) await Share.open({ diff --git a/components/settings/import-export/import-dialog.js b/components/settings/data-management/import-dialog.js similarity index 100% rename from components/settings/import-export/import-dialog.js rename to components/settings/data-management/import-dialog.js diff --git a/components/settings/data-management/index.js b/components/settings/data-management/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4199760d27af53c3c1019dc6aab0855b0f952c20 --- /dev/null +++ b/components/settings/data-management/index.js @@ -0,0 +1,38 @@ +import React from 'react' +import { ScrollView } from 'react-native' +import AppText from '../../app-text' +import SettingsSegment from '../settings-segment' +import SettingsButton from '../settings-button' +import openImportDialogAndImport from './import-dialog' +import openShareDialogAndExport from './export-dialog' +import DeleteData from './delete-data' +import labels from '../../../i18n/en/settings' +import styles from '../../../styles/index' + +const DataManagement = () => { + return ( + <ScrollView> + <SettingsSegment title={labels.export.button}> + <AppText>{labels.export.segmentExplainer}</AppText> + <SettingsButton onPress={openShareDialogAndExport}> + {labels.export.button} + </SettingsButton> + </SettingsSegment> + <SettingsSegment title={labels.import.button}> + <AppText>{labels.import.segmentExplainer}</AppText> + <SettingsButton onPress={openImportDialogAndImport}> + {labels.import.button} + </SettingsButton> + </SettingsSegment> + <SettingsSegment + title={labels.deleteSegment.title} + style={styles.settingsSegmentLast} + > + <AppText>{labels.deleteSegment.explainer}</AppText> + <DeleteData /> + </SettingsSegment> + </ScrollView> + ) +} + +export default DataManagement \ No newline at end of file diff --git a/components/settings/import-export/index.js b/components/settings/import-export/index.js deleted file mode 100644 index 430f29b50dae183b962da3a07df9af8413795bbb..0000000000000000000000000000000000000000 --- a/components/settings/import-export/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { Component } from 'react' -import { - View, ScrollView, - TouchableOpacity, -} from 'react-native' -import styles from '../../../styles/index' -import labels from '../../../i18n/en/settings' -import AppText from '../../app-text' -import openImportDialogAndImport from './import-dialog' -import openShareDialogAndExport from './export-dialog' - -export default class Settings extends Component { - constructor(props) { - super(props) - this.state = {} - } - - render() { - return ( - <ScrollView> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.export.button} - </AppText> - <AppText>{labels.export.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openShareDialogAndExport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.export.button} - </AppText> - </TouchableOpacity> - </View> - <View style={styles.settingsSegment}> - <AppText style={styles.settingsSegmentTitle}> - {labels.import.button} - </AppText> - <AppText>{labels.import.segmentExplainer}</AppText> - <TouchableOpacity - onPress={openImportDialogAndImport} - style={styles.settingsButton}> - <AppText style={styles.settingsButtonText}> - {labels.import.button} - </AppText> - </TouchableOpacity> - </View> - </ScrollView> - ) - } -} diff --git a/components/settings/index.js b/components/settings/index.js index a1eaa03ee5bc9f1218da31b773e16c208679689f..7bbbdc53f7911744e4700656b0a5fc86e813f0dd 100644 --- a/components/settings/index.js +++ b/components/settings/index.js @@ -1,9 +1,9 @@ import Reminders from './reminders' import NfpSettings from './nfp-settings' -import ImportExport from './import-export' +import DataManagement from './data-management' import Password from './password' import About from './about' export default { - Reminders, NfpSettings, ImportExport, Password, About + Reminders, NfpSettings, DataManagement, Password, About } diff --git a/components/settings/password/check-current-password.js b/components/settings/password/check-current-password.js index 76ddc9453abed9608ce05704adfd6cedf853b3af..92d78bf99effcfc56cfb4afd758bbc7e16371ab3 100644 --- a/components/settings/password/check-current-password.js +++ b/components/settings/password/check-current-password.js @@ -3,18 +3,21 @@ import { openDb } from '../../../db' import { shared } from '../../../i18n/en/labels' export default async function checkPassword({hash, onCancel, onTryAgain }) { - const connected = await openDb(hash) - if (connected) return true - Alert.alert( - shared.incorrectPassword, - shared.incorrectPasswordMessage, - [{ - text: shared.cancel, - onPress: onCancel - }, { - text: shared.tryAgain, - onPress: onTryAgain - }] - ) - return false + try { + await openDb(hash) + return true + } catch (err) { + Alert.alert( + shared.incorrectPassword, + shared.incorrectPasswordMessage, + [{ + text: shared.cancel, + onPress: onCancel + }, { + text: shared.tryAgain, + onPress: onTryAgain + }] + ) + return false + } } \ No newline at end of file diff --git a/components/settings/password/create.js b/components/settings/password/create.js index bda688f83ebbb02cf5961b0a98bcb4382069f2cb..cdc9b69c3a9e85cfe9925cf10ecd73c04fc76247 100644 --- a/components/settings/password/create.js +++ b/components/settings/password/create.js @@ -2,7 +2,7 @@ import React, { Component } from 'react' import { View } from 'react-native' import settings from '../../../i18n/en/settings' import EnterNewPassword from './enter-new-password' -import SettingsButton from './settings-button' +import SettingsButton from '../settings-button' import showBackUpReminder from './show-backup-reminder' export default class CreatePassword extends Component { diff --git a/components/settings/password/enter-new-password.js b/components/settings/password/enter-new-password.js index ed09ead3328b7bf56e153cfb7d7750c5e5b0c96f..084a6bc671f6f11f765520228f794a75d45dd757 100644 --- a/components/settings/password/enter-new-password.js +++ b/components/settings/password/enter-new-password.js @@ -5,7 +5,7 @@ import nodejs from 'nodejs-mobile-react-native' import { requestHash, changeEncryptionAndRestartApp } from '../../../db' import AppText from '../../app-text' import PasswordField from './password-field' -import SettingsButton from './settings-button' +import SettingsButton from '../settings-button' import styles from '../../../styles' import settings from '../../../i18n/en/settings' diff --git a/components/settings/password/index.js b/components/settings/password/index.js index f5741e3f372027f471bba9687a25501753edc193..fca09664f5cd06f6dce2ac4e61430fd9ce3e4def 100644 --- a/components/settings/password/index.js +++ b/components/settings/password/index.js @@ -3,11 +3,11 @@ import { View, ScrollView } from 'react-native' import CreatePassword from './create' import ChangePassword from './update' import DeletePassword from './delete' +import SettingsSegment from '../settings-segment' import AppText from '../../app-text' import { hasEncryptionObservable } from '../../../local-storage' -import styles from '../../../styles/index' import labels from '../../../i18n/en/settings' export default class PasswordSetting extends Component { @@ -22,11 +22,7 @@ export default class PasswordSetting extends Component { render() { return ( <ScrollView> - <View style={styles.settingsSegment}> - - <AppText style={styles.settingsSegmentTitle}> - {labels.passwordSettings.title} - </AppText> + <SettingsSegment title={labels.passwordSettings.title}> {this.state.showUpdateAndDelete ? <AppText>{labels.passwordSettings.explainerEnabled}</AppText> @@ -45,7 +41,7 @@ export default class PasswordSetting extends Component { <CreatePassword/> } - </View> + </SettingsSegment> </ScrollView> ) } diff --git a/components/settings/password/update.js b/components/settings/password/update.js index a246c0ea6dc6c8e3d83bc96b377bbe26a2a17148..bac6d2bcf2d41e51c1d1f4aa6cb58fcad638de54 100644 --- a/components/settings/password/update.js +++ b/components/settings/password/update.js @@ -6,7 +6,7 @@ import settings from '../../../i18n/en/settings' import { requestHash } from '../../../db' import EnterNewPassword from './enter-new-password' import PasswordField from './password-field' -import SettingsButton from './settings-button' +import SettingsButton from '../settings-button' import showBackUpReminder from './show-backup-reminder' import checkCurrentPassword from './check-current-password' diff --git a/components/settings/password/settings-button.js b/components/settings/settings-button.js similarity index 88% rename from components/settings/password/settings-button.js rename to components/settings/settings-button.js index 97272bb2bb6cb603d97fcd37b97408d808ca7582..bfa89832f2919ac86e4c88f559851c4aecb3f042 100644 --- a/components/settings/password/settings-button.js +++ b/components/settings/settings-button.js @@ -2,8 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import { TouchableOpacity } from 'react-native' -import AppText from '../../app-text' -import styles from '../../../styles' +import AppText from '../app-text' +import styles from '../../styles' const SettingsButton = ({ children, ...props }) => { return ( diff --git a/components/settings/settings-menu.js b/components/settings/settings-menu.js index 4152c11f6a56558cadc8dbb452b860db80f56223..3d927e0329e017e4edb30086fcfad60c01020ecf 100644 --- a/components/settings/settings-menu.js +++ b/components/settings/settings-menu.js @@ -7,14 +7,12 @@ import styles from '../../styles/index' import settingsLabels from '../../i18n/en/settings' import AppText from '../app-text' -console.log(settingsLabels.menuTitles) const labels = settingsLabels.menuTitles -console.log(settingsLabels.menuTitles) const menu = [ {title: labels.reminders, component: 'Reminders'}, {title: labels.nfpSettings, component: 'NfpSettings'}, - {title: labels.importExport, component: 'ImportExport'}, + {title: labels.dataManagement, component: 'DataManagement'}, {title: labels.password, component: 'Password'}, {title: labels.about, component: 'About'} ] diff --git a/components/settings/settings-segment.js b/components/settings/settings-segment.js new file mode 100644 index 0000000000000000000000000000000000000000..0a8771987e2ea29ac5676bce9d6ba092e4ba9cb6 --- /dev/null +++ b/components/settings/settings-segment.js @@ -0,0 +1,21 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { View } from 'react-native' +import AppText from '../app-text' +import styles from '../../styles' + +const SettingsSegment = ({ children, ...props }) => { + return ( + <View style={[styles.settingsSegment, props.style]}> + <AppText style={styles.settingsSegmentTitle}>{props.title}</AppText> + {children} + </View> + ) +} + +SettingsSegment.propTypes = { + title: PropTypes.string.isRequired +} + +export default SettingsSegment \ No newline at end of file diff --git a/db/index.js b/db/index.js index ca9752eeab5c3263f2e5cc8c4216f83239371ac2..5fcbab0e85730efa1b6a8ad7da70cfdcf8dd20cf 100644 --- a/db/index.js +++ b/db/index.js @@ -223,12 +223,20 @@ export async function changeEncryptionAndRestartApp(hash) { restart.Restart() } +export function isDbEmpty () { + return db.empty +} + export async function deleteDbAndOpenNew() { const exists = await fs.exists(Realm.defaultPath) if (exists) await fs.unlink(Realm.defaultPath) await openDb() } +export function clearDb() { + db.write(db.deleteAll) +} + function hashToInt8Array(hash) { const key = new Uint8Array(64) for (let i = 0; i < key.length; i++) { diff --git a/i18n/en/labels.js b/i18n/en/labels.js index 743f4c12023c3a0791922237674c6988cd005e2d..17e35260e88282fca23a989aed4c7efc7cff40f4 100644 --- a/i18n/en/labels.js +++ b/i18n/en/labels.js @@ -27,7 +27,7 @@ export const headerTitles = { SettingsMenu: 'Settings', Reminders: settingsTitles.reminders, NfpSettings: settingsTitles.nfpSettings, - ImportExport: settingsTitles.importExport, + DataManagement: settingsTitles.dataManagement, Password: settingsTitles.password, About: settingsTitles.about, BleedingEditView: 'Bleeding', diff --git a/i18n/en/settings.js b/i18n/en/settings.js index 9d46eb45942cc4227f0a92b21e5d199e44532873..d3565d380848266f158c964037576650438b5034 100644 --- a/i18n/en/settings.js +++ b/i18n/en/settings.js @@ -2,7 +2,7 @@ export default { menuTitles: { reminders: 'Reminders', - importExport: 'Import and Export', + dataManagement: 'Manage your data', nfpSettings: 'NFP settings', password: 'Password', about: 'About' @@ -35,6 +35,21 @@ export default { }, segmentExplainer: 'Import data in CSV format' }, + deleteSegment: { + title: 'Delete app data', + explainer: 'Delete app data from this phone', + question: 'Do you want to delete app data from this phone?', + message: 'Please note that deletion of the app data is permanent and irreversible. We recommend exporting existing data before deletion.', + confirmation: 'Delete app data permanently', + errors: { + couldNotDeleteFile: 'Could not delete data', + postFix: 'No data was deleted or changed', + noData: 'There is no data to delete' + }, + success: { + message: 'App data successfully deleted' + } + }, tempScale: { segmentTitle: 'Temperature scale', segmentExplainer: 'Change the minimum and maximum value for the temperature chart',