import React, { useState, useEffect, useRef } from 'react'; import { connect } from 'react-redux'; import { Route, Switch, Redirect } from 'react-router-dom'; import { css } from 'glamor'; import * as actions from 'loot-core/src/client/actions'; import Platform from 'loot-core/src/client/platform'; import { send, listen } from 'loot-core/src/platform/client/fetch'; import { numberFormats } from 'loot-core/src/shared/util'; import { Information } from 'loot-design/src/components/alerts'; import { View, Text, Button, ButtonWithLoading, AnchorLink } from 'loot-design/src/components/common'; import { styles, colors } from 'loot-design/src/style'; import ExpandArrow from 'loot-design/src/svg/ExpandArrow'; import useServerVersion from '../hooks/useServerVersion'; let dateFormats = [ { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' }, { value: 'dd/MM/yyyy', label: 'DD/MM/YYYY' }, { value: 'yyyy-MM-dd', label: 'YYYY-MM-DD' }, { value: 'MM.dd.yyyy', label: 'MM.DD.YYYY' }, { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' } ]; function Title({ name, style }) { return ( {name} ); } function Advanced({ prefs, userData, pushModal, resetSync }) { let [expanded, setExpanded] = useState(true); let [resetting, setResetting] = useState(false); let [resettingCache, setResettingCache] = useState(false); async function onResetSync() { setResetting(true); await resetSync(); setResetting(false); } async function onResetCache() { setResettingCache(true); await send('reset-budget-cache'); setResettingCache(false); } return ( setExpanded(!expanded)} > Advanced {expanded && ( Budget ID: {prefs.id} Reset budget cache will clear all cached values for the budget and recalculate the entire budget. All values in the budget are cached for performance reasons, and if there is a bug in the cache you won't see correct values. There is no danger in resetting the cache. Hopefully you never have to do this. Reset budget cache Reset sync will remove all local data used to track changes for syncing, and create a fresh sync id on our server. This file on other devices will have to be re-downloaded to use the new sync id. Use this if there is a problem with syncing and you want to start fresh. Reset sync Sync ID: {prefs.groupId || '(none)'} )} ); } function GlobalSettings({ globalPrefs, userData, saveGlobalPrefs, pushModal, closeBudget }) { let [documentDirChanged, setDirChanged] = useState(false); let dirScrolled = useRef(null); useEffect(() => { if (dirScrolled.current) { dirScrolled.current.scrollTo(10000, 0); } }, []); async function onChooseDocumentDir() { let res = await window.Actual.openFileDialog({ properties: ['openDirectory'] }); if (res) { saveGlobalPrefs({ documentDir: res[0] }); setDirChanged(true); } } function onAutoUpdate(e) { saveGlobalPrefs({ autoUpdate: e.target.checked }); } function onTrackUsage(e) { saveGlobalPrefs({ trackUsage: e.target.checked }); } return ( {!Platform.isBrowser && ( <View style={{ flexDirection: 'row', maxWidth: 550, alignItems: 'center', overflow: 'hidden' }} > <Text style={{ flexShrink: 0 }}>Store files here: </Text> <Text innerRef={dirScrolled} style={{ backgroundColor: 'white', padding: '7px 10px', borderRadius: 4, marginLeft: 5, overflow: 'auto', whiteSpace: 'nowrap', // TODO: When we update electron, we should be able to // remove this. In previous versions of Chrome, once the // scrollbar appears it never goes away '::-webkit-scrollbar': { display: 'none' } }} > {globalPrefs.documentDir} </Text> <Button primary onClick={onChooseDocumentDir} style={{ fontSize: 14, marginLeft: 5, flexShrink: 0, alignSelf: 'flex-start' }} > Change location </Button> </View> )} {documentDirChanged && ( <Information style={{ marginTop: 10 }}> A restart is required for this change to take effect </Information> )} <View style={{ flexDirection: 'row', marginTop: 30, alignItems: 'flex-start' }} > <input type="checkbox" checked={globalPrefs.autoUpdate} style={{ marginRight: 5 }} onChange={onAutoUpdate} /> <View> <Text style={{ fontSize: 15 }}> Automatically check for updates </Text> <View style={{ color: colors.n2, marginTop: 10, maxWidth: 600, lineHeight: '1.4em' }} > By default, Actual will automatically apply new updates as they are available. Disabling this will avoid updating Actual. You will need to go to the About menu to manually check for updates. </View> </View> </View> </View> <View style={{ marginTop: 30 }}> <Title name="Privacy" /> <View style={{ flexDirection: 'row', marginTop: 30, alignItems: 'flex-start' }} > <input type="checkbox" checked={globalPrefs.trackUsage} style={{ marginRight: 5 }} onChange={onTrackUsage} /> <View> <Text style={{ fontSize: 15 }}> Send basic usage statistics back to Actual{"'"}s servers </Text> <View style={{ color: colors.n2, marginTop: 10, maxWidth: 600, lineHeight: '1.4em' }} > We don{"'"}t track anything specific — only the fact that you{"'"}ve opened Actual. This helps by giving us important feedback about how popular new features are. </View> </View> </View> </View> </View> ); } function FileSettings({ savePrefs, prefs, userData, localServerURL, pushModal, resetSync, setAppState, signOut }) { function onDateFormat(e) { let format = e.target.value; savePrefs({ dateFormat: format }); } function onNumberFormat(e) { let format = e.target.value; savePrefs({ numberFormat: format }); } function onChangeKey() { pushModal('create-encryption-key', { recreate: true }); } async function onExport() { let data = await send('export-budget'); window.Actual.saveFile(data, `${prefs.id}.zip`, 'Export budget'); } let dateFormat = prefs.dateFormat || 'MM/dd/yyyy'; let numberFormat = prefs.numberFormat || 'comma-dot'; return ( <View> <View style={{ marginTop: 30 }}> <Title name="Formatting" /> <Text> Date format:{' '} <select {...css({ marginLeft: 5, fontSize: 14 })} onChange={onDateFormat} > {dateFormats.map(f => ( <option value={f.value} selected={f.value === dateFormat}> {f.label} </option> ))} </select> </Text> <Text style={{ marginTop: 20 }}> Number format:{' '} <select {...css({ marginLeft: 5, fontSize: 14 })} onChange={onNumberFormat} > {numberFormats.map(f => ( <option value={f.value} selected={f.value === numberFormat}> {f.label} </option> ))} </select> </Text> </View> <View style={{ marginTop: 30 }}> <Title name="Encryption" /> <View style={{ flexDirection: 'row' }}> <View> <Text style={{ fontWeight: 700, fontSize: 15 }}> End-to-end encryption </Text> <View style={{ color: colors.n2, marginTop: 10, maxWidth: 600, lineHeight: '1.4em' }} > {prefs.encryptKeyId ? ( <Text> <Text style={{ color: colors.g4, fontWeight: 600 }}> Encryption is turned on. </Text>{' '} Your data is encrypted with a key that only you have before sending it out to the cloud . Local data remains unencrypted so if you forget your password you can re-encrypt it. <Button style={{ marginTop: 10 }} onClick={() => onChangeKey()} > Generate new key </Button> </Text> ) : ( <View style={{ alignItems: 'flex-start' }}> <Text style={{ lineHeight: '1.4em' }}> Encryption is not enabled. Any data on our servers is still stored safely and securely, but it's not end-to-end encrypted which means we have the ability to read it (but we won't). If you want, you can use a password to encrypt your data on our servers. </Text> <Button style={{ marginTop: 10 }} onClick={() => { alert( 'End-to-end encryption is not supported on the self-hosted service yet' ); // pushModal('create-encryption-key'); }} > Enable encryption </Button> </View> )} </View> </View> </View> </View> <View style={{ marginTop: 30, alignItems: 'flex-start' }}> <Title name="Export" /> <Button onClick={onExport}>Export data</Button> </View> <Advanced prefs={prefs} userData={userData} pushModal={pushModal} resetSync={resetSync} /> </View> ); } function SettingsLink({ to, name, style, first, last }) { return ( <AnchorLink to={to} style={[ { fontSize: 14, padding: '6px 10px', borderBottom: '2px solid transparent', textDecoration: 'none', borderRadius: first ? '4px 0 0 4px' : last ? '0 4px 4px 0' : 4, border: '1px solid ' + colors.n4, color: colors.n3 }, style ]} activeStyle={{ backgroundColor: colors.p6, borderColor: colors.p6, color: 'white' }} > {name} </AnchorLink> ); } function Version() { const version = useServerVersion(); return ( <Text style={[ { alignSelf: 'center', color: colors.n7, ':hover': { color: colors.n2 }, padding: '6px 10px' }, styles.staticText, styles.smallText ]} > {`App: v${window.Actual.ACTUAL_VERSION} | Server: ${version}`} </Text> ); } class Settings extends React.Component { componentDidMount() { this.unlisten = listen('prefs-updated', () => { this.props.loadPrefs(); }); this.props.getUserData(); this.props.loadPrefs(); } componentWillUnmount() { this.unlisten(); } render() { let { prefs, globalPrefs, localServerURL, userData, match } = this.props; return ( <View style={[styles.page, { overflow: 'hidden', fontSize: 14 }]}> <View style={{ flexDirection: 'row', alignSelf: 'center', margin: '15px 0 5px 0' }} > <SettingsLink to={`${match.path}/file`} name="File" first={true} /> <SettingsLink to={`${match.path}/global`} name="Global" last={true} /> </View> <View style={{ flexDirection: 'row', alignSelf: 'center', margin: '0 0 10px 0' }} > <Version /> </View> <View style={[ styles.pageContent, { alignItems: 'flex-start', flex: 1, overflow: 'auto', paddingBottom: 20 } ]} > <View style={{ flexShrink: 0 }}> <Switch> <Route path={`${match.path}/`} exact> <Redirect to={`${match.path}/file`} /> </Route> <Route path={`${match.path}/global`}> <GlobalSettings globalPrefs={globalPrefs} userData={userData} saveGlobalPrefs={this.props.saveGlobalPrefs} pushModal={this.props.pushModal} closeBudget={this.props.closeBudget} /> </Route> <Route path={`${match.path}/file`}> <FileSettings prefs={prefs} localServerURL={localServerURL} userData={userData} pushModal={this.props.pushModal} savePrefs={this.props.savePrefs} setAppState={this.props.setAppState} signOut={this.props.signOut} resetSync={this.props.resetSync} /> </Route> </Switch> </View> </View> </View> ); } } export default connect( state => ({ prefs: state.prefs.local, globalPrefs: state.prefs.global, localServerURL: state.account.localServerURL, userData: state.user.data }), actions )(Settings);