From 6310d9230b572e649ecc7328b84963c3185898b8 Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Fri, 30 Dec 2022 13:05:58 -0500 Subject: [PATCH] Simplify the settings layout and merge everything onto one page (#267) Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv> --- .../desktop-client/src/components/Settings.js | 563 ++++++------------ packages/loot-core/src/mocks/setup.js | 4 - packages/loot-core/src/server/main.js | 38 -- .../loot-core/src/server/tracking/events.js | 61 -- .../mobile/src/components/manager/Login.js | 6 +- 5 files changed, 183 insertions(+), 489 deletions(-) delete mode 100644 packages/loot-core/src/server/tracking/events.js diff --git a/packages/desktop-client/src/components/Settings.js b/packages/desktop-client/src/components/Settings.js index 616daca20..e93656401 100644 --- a/packages/desktop-client/src/components/Settings.js +++ b/packages/desktop-client/src/components/Settings.js @@ -13,6 +13,7 @@ import { View, Text, Button, + Link, ButtonWithLoading, AnchorLink } from 'loot-design/src/components/common'; @@ -20,6 +21,7 @@ import { styles, colors } from 'loot-design/src/style'; import ExpandArrow from 'loot-design/src/svg/ExpandArrow'; import useServerVersion from '../hooks/useServerVersion'; +import { Page } from './Page'; let dateFormats = [ { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' }, @@ -29,23 +31,49 @@ let dateFormats = [ { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' } ]; -function Title({ name, style }) { +function Section({ title, children, style, titleProps, ...props }) { + return ( + <View style={[{ gap: 20, alignItems: 'flex-start' }, style]} {...props}> + <View + style={[ + { fontSize: 20, fontWeight: 500, flexShrink: 0 }, + titleProps && titleProps.style + ]} + {...titleProps} + > + {title} + </View> + {children} + </View> + ); +} + +function ButtonSetting({ button, children, onClick }) { return ( <View - style={[ - { fontSize: 20, fontWeight: 500, marginBottom: 20, flexShrink: 0 }, - style - ]} + style={{ + backgroundColor: colors.n9, + alignSelf: 'flex-start', + alignItems: 'flex-start', + padding: 15, + borderRadius: 4, + border: '1px solid ' + colors.n8 + }} > - {name} + <View + style={{ marginBottom: 10, maxWidth: 500, lineHeight: 1.5, gap: 10 }} + > + {children} + </View> + {button} </View> ); } -function Advanced({ prefs, userData, pushModal, resetSync }) { - let [expanded, setExpanded] = useState(true); +function Advanced({ prefs, resetSync }) { let [resetting, setResetting] = useState(false); let [resettingCache, setResettingCache] = useState(false); + let [expanded, setExpanded] = useState(false); async function onResetSync() { setResetting(true); @@ -59,98 +87,56 @@ function Advanced({ prefs, userData, pushModal, resetSync }) { setResettingCache(false); } - return ( - <View style={{ alignItems: 'flex-start', marginTop: 55 }}> - <View - style={[ - { - fontSize: 15, - marginBottom: 20, - flexDirection: 'row', - alignItems: 'center' - }, - styles.staticText - ]} - onClick={() => setExpanded(!expanded)} + return expanded ? ( + <Section title="Advanced Settings" style={{ marginBottom: 25 }}> + <Text>Budget ID: {prefs.id}</Text> + <Text style={{ color: colors.n6 }}> + Sync ID: {prefs.groupId || '(none)'} + </Text> + + <ButtonSetting + button={ + <ButtonWithLoading loading={resettingCache} onClick={onResetCache}> + Reset budget cache + </ButtonWithLoading> + } > - <ExpandArrow - width={8} - height={8} - style={{ - marginRight: 5, - transition: 'transform .2s', - transform: !expanded && 'rotateZ(-90deg)' - }} - /> - Advanced - </View> - - {expanded && ( - <View style={{ marginBottom: 20, alignItems: 'flex-start' }}> - <Text> - <strong>Budget ID</strong>: {prefs.id} - </Text> - - <View - style={{ - backgroundColor: colors.n9, - alignItems: 'flex-start', - padding: 15, - borderRadius: 4, - marginTop: 20, - border: '1px solid ' + colors.n8 - }} - > - <Text style={{ marginBottom: 10, width: 500, lineHeight: 1.5 }}> - <strong>Reset budget cache</strong> 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. - </Text> - <ButtonWithLoading loading={resettingCache} onClick={onResetCache}> - Reset budget cache - </ButtonWithLoading> - </View> - - <View - style={{ - backgroundColor: colors.n9, - alignItems: 'flex-start', - padding: 15, - borderRadius: 4, - marginTop: 20, - border: '1px solid ' + colors.n8 - }} - > - <Text style={{ marginBottom: 10, width: 500, lineHeight: 1.5 }}> - <strong>Reset sync</strong> 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. - </Text> - - <ButtonWithLoading loading={resetting} onClick={onResetSync}> - Reset sync - </ButtonWithLoading> - <Text style={{ marginTop: 15, color: colors.n4, fontSize: 12 }}> - Sync ID: {prefs.groupId || '(none)'} - </Text> - </View> - </View> - )} - </View> + <Text> + <strong>Reset budget cache</strong> 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. + </Text> + </ButtonSetting> + + <ButtonSetting + button={ + <ButtonWithLoading loading={resetting} onClick={onResetSync}> + Reset sync + </ButtonWithLoading> + } + > + <Text> + <strong>Reset sync</strong> 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. + </Text> + </ButtonSetting> + </Section> + ) : ( + <Link + onClick={() => setExpanded(true)} + style={{ flexShrink: 0, alignSelf: 'flex-start', color: colors.p4 }} + > + Show advanced settings + </Link> ); } -function GlobalSettings({ - globalPrefs, - userData, - saveGlobalPrefs, - pushModal, - closeBudget -}) { +function GlobalSettings({ globalPrefs, saveGlobalPrefs }) { let [documentDirChanged, setDirChanged] = useState(false); let dirScrolled = useRef(null); @@ -170,24 +156,13 @@ function GlobalSettings({ } } - function onAutoUpdate(e) { - saveGlobalPrefs({ autoUpdate: e.target.checked }); - } - - function onTrackUsage(e) { - saveGlobalPrefs({ trackUsage: e.target.checked }); - } - return ( - <View> - <View> - <Title name="General" /> - - {!Platform.isBrowser && ( + <> + {!Platform.isBrowser && ( + <Section title="General"> <View style={{ flexDirection: 'row', - maxWidth: 550, alignItems: 'center', overflow: 'hidden' }} @@ -223,98 +198,19 @@ function GlobalSettings({ 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> + )} + {documentDirChanged && ( + <Information style={{ marginTop: 10 }}> + A restart is required for this change to take effect + </Information> + )} + </Section> + )} + </> ); } -function FileSettings({ - savePrefs, - prefs, - userData, - localServerURL, - pushModal, - resetSync, - setAppState, - signOut -}) { +function FileSettings({ savePrefs, prefs, pushModal, resetSync }) { function onDateFormat(e) { let format = e.target.value; savePrefs({ dateFormat: format }); @@ -336,158 +232,108 @@ function FileSettings({ let dateFormat = prefs.dateFormat || 'MM/dd/yyyy'; let numberFormat = prefs.numberFormat || 'comma-dot'; - return ( - <View> - <View style={{ marginTop: 30 }}> - <Title name="Formatting" /> - + <> + <Section title="Formatting"> <Text> - Date format:{' '} + <label for="settings-numberFormat">Number format: </label> <select + id="settings-numberFormat" {...css({ marginLeft: 5, fontSize: 14 })} - onChange={onDateFormat} + onChange={onNumberFormat} > - {dateFormats.map(f => ( - <option value={f.value} selected={f.value === dateFormat}> + {numberFormats.map(f => ( + <option value={f.value} selected={f.value === numberFormat}> {f.label} </option> ))} </select> </Text> - <Text style={{ marginTop: 20 }}> - Number format:{' '} + <Text> + <label for="settings-dateFormat">Date format: </label> <select + id="settings-dateFormat" {...css({ marginLeft: 5, fontSize: 14 })} - onChange={onNumberFormat} + onChange={onDateFormat} > - {numberFormats.map(f => ( - <option value={f.value} selected={f.value === numberFormat}> + {dateFormats.map(f => ( + <option value={f.value} selected={f.value === dateFormat}> {f.label} </option> ))} </select> </Text> - </View> + </Section> - <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 ? ( + <ButtonSetting + button={ + <Button onClick={() => onChangeKey()}>Generate new key</Button> + } + > + <Text> + <Text style={{ color: colors.g4, fontWeight: 600 }}> + End-to-end 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. + </Text> + </ButtonSetting> + ) : ( + <ButtonSetting + button={ + <Button + onClick={() => { + alert( + 'End-to-end encryption is not supported on the self-hosted service yet' + ); + // pushModal('create-encryption-key'); }} > - {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> + Enable encryption… + </Button> + } + > + <Text> + <strong>End-to-end encryption</strong> 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> + </ButtonSetting> + )} - <Advanced - prefs={prefs} - userData={userData} - pushModal={pushModal} - resetSync={resetSync} - /> - </View> - ); -} + <ButtonSetting button={<Button onClick={onExport}>Export data</Button>}> + <Text> + <strong>Export</strong> your data as a zip file containing{' '} + <code>db.sqlite</code> and <code>metadata.json</code> files. It can be + imported into another Actual instance by clicking the “Import file†+ button and then choosing “Actual†on the Files page. + </Text> + {prefs.encryptKeyId ? ( + <Text> + Even though encryption is enabled, the exported zip file will not + have any encryption. + </Text> + ) : null} + </ButtonSetting> -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> + <Advanced prefs={prefs} resetSync={resetSync} /> + </> ); } -function Version() { +function About() { 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> + <Section title="About"> + <Text>Client version: v{window.Actual.ACTUAL_VERSION}</Text> + <Text>Server version: {version}</Text> + </Section> ); } @@ -509,68 +355,23 @@ class Settings extends React.Component { 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> + <Page title="Settings"> + <View style={{ flexShrink: 0, gap: 30, maxWidth: 600 }}> + <About /> - <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> + <GlobalSettings + globalPrefs={globalPrefs} + saveGlobalPrefs={this.props.saveGlobalPrefs} + /> + + <FileSettings + prefs={prefs} + userData={userData} + pushModal={this.props.pushModal} + resetSync={this.props.resetSync} + /> </View> - </View> + </Page> ); } } diff --git a/packages/loot-core/src/mocks/setup.js b/packages/loot-core/src/mocks/setup.js index 172aea37f..728293138 100644 --- a/packages/loot-core/src/mocks/setup.js +++ b/packages/loot-core/src/mocks/setup.js @@ -9,15 +9,11 @@ import { import { setServer } from '../server/server-config'; import * as sheet from '../server/sheet'; import { setSyncingMode } from '../server/sync'; -import * as tracking from '../server/tracking/events'; import { updateVersion } from '../server/update'; import { resetTracer, tracer } from '../shared/test-helpers'; jest.mock('../server/post'); -// No need to run any of the tracking code in tests -tracking.toggle(false); - const nativeFs = require('fs'); // By default, syncing is disabled diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.js index f7f4106af..2ff712dcd 100644 --- a/packages/loot-core/src/server/main.js +++ b/packages/loot-core/src/server/main.js @@ -68,7 +68,6 @@ import { } from './sync'; import * as syncMigrations from './sync/migrate'; import toolsApp from './tools/app'; -import * as tracking from './tracking/events'; import { withUndo, clearUndo, undo, redo } from './undo'; import { updateVersion } from './update'; import { uniqueFileName, idFromFileName } from './util/budget-name'; @@ -1212,10 +1211,6 @@ handlers['save-global-prefs'] = async function(prefs) { if ('maxMonths' in prefs) { await asyncStorage.setItem('max-months', '' + prefs.maxMonths); } - if ('trackUsage' in prefs) { - tracking.toggle(prefs.trackUsage); - await asyncStorage.setItem('track-usage', '' + prefs.trackUsage); - } if ('autoUpdate' in prefs) { await asyncStorage.setItem('auto-update', '' + prefs.autoUpdate); process.send({ type: 'shouldAutoUpdate', flag: prefs.autoUpdate }); @@ -1236,7 +1231,6 @@ handlers['load-global-prefs'] = async function() { [, floatingSidebar], [, seenTutorial], [, maxMonths], - [, trackUsage], [, autoUpdate], [, documentDir], [, encryptKey] @@ -1244,7 +1238,6 @@ handlers['load-global-prefs'] = async function() { 'floating-sidebar', 'seen-tutorial', 'max-months', - 'track-usage', 'auto-update', 'document-dir', 'encrypt-key' @@ -1253,8 +1246,6 @@ handlers['load-global-prefs'] = async function() { floatingSidebar: floatingSidebar === 'true' ? true : false, seenTutorial: seenTutorial === 'true' ? true : false, maxMonths: stringToInteger(maxMonths || ''), - // Default to true - trackUsage: trackUsage == null || trackUsage === 'true' ? true : false, autoUpdate: autoUpdate == null || autoUpdate === 'true' ? true : false, documentDir: documentDir || getDefaultDocumentDir(), keyId: encryptKey && JSON.parse(encryptKey).id @@ -1687,26 +1678,6 @@ handlers['load-budget'] = async function({ id }) { let res = await loadBudget(id, VERSION, { showUpdate: true }); - async function trackSizes() { - let getFileSize = async name => { - let dbFile = fs.join(fs.getBudgetDir(id), name); - try { - return await fs.size(dbFile); - } catch (err) { - return null; - } - }; - - try { - let dbSize = await getFileSize('db.sqlite'); - let cacheSize = await getFileSize('cache.sqlite'); - tracking.track('app:load-budget', { size: dbSize, cacheSize }); - } catch (err) { - console.warn(err); - } - } - trackSizes(); - return res; }; @@ -2104,10 +2075,6 @@ handlers['app-focused'] = async function() { } }; -handlers['track'] = async function({ name, props }) { - tracking.track(name, props); -}; - handlers = installAPI(handlers); injectAPI.send = (name, args) => runHandler(app.handlers[name], args); @@ -2160,7 +2127,6 @@ export async function initApp(version, isDev, socketName) { await sqlite.init(); await Promise.all([asyncStorage.init(), fs.init()]); - await tracking.init(); await setupDocumentsDir(); let keysStr = await asyncStorage.getItem('encrypt-keys'); @@ -2194,10 +2160,6 @@ export async function initApp(version, isDev, socketName) { connection.init(socketName, app.handlers); - tracking.track('app:init', { - platform: Platform.isMobile ? 'mobile' : Platform.isWeb ? 'web' : 'desktop' - }); - if (!isDev && !Platform.isMobile && !Platform.isWeb) { let autoUpdate = await asyncStorage.getItem('auto-update'); process.send({ diff --git a/packages/loot-core/src/server/tracking/events.js b/packages/loot-core/src/server/tracking/events.js deleted file mode 100644 index fea5dd6e2..000000000 --- a/packages/loot-core/src/server/tracking/events.js +++ /dev/null @@ -1,61 +0,0 @@ -import { sha256String } from '../encryption-internals'; - -let currentUniqueId; -let mixpanel; -let isEnabled = true; - -export function toggle(trackUsage) { - isEnabled = trackUsage == null || trackUsage === 'true' ? true : false; -} - -// TODO: Figure out location, send to EU data centers if in EU -// { -// host: "api-eu.mixpanel.com", -// }, - -// This must stay up-to-date with all apps that hit mixpanel! That includes the -// website and server. If changing this, make sure to change it everywhere -async function hash(userId) { - let hashed = await sha256String(userId); - return `user-${hashed.replace(/[=/]/g, '')}`; -} - -function isAnonymous(id) { - return !id.startsWith('user-'); -} - -export async function init() {} - -export async function login(userId) {} - -let BUFFERING = false; -let BUFFER = []; - -function startBuffering() { - BUFFERING = true; - BUFFER = []; -} - -function stopBuffering() { - for (let call of BUFFER) { - call[0](...call[1]); - } - BUFFERING = false; - BUFFER = []; -} - -function buffered(func) { - return (...args) => { - if (process.env.NODE_ENV !== 'development') { - if (BUFFERING) { - BUFFER.push([func, [currentUniqueId, ...args]]); - } else { - func(currentUniqueId, ...args); - } - } - }; -} - -export const track = buffered((distinct_id, name, props) => {}); - -export const setProfile = buffered((distinct_id, props) => {}); diff --git a/packages/mobile/src/components/manager/Login.js b/packages/mobile/src/components/manager/Login.js index 2c114ca51..3f835702b 100644 --- a/packages/mobile/src/components/manager/Login.js +++ b/packages/mobile/src/components/manager/Login.js @@ -55,7 +55,6 @@ function Login({ navigation, createBudget }) { navigation={navigation} buttons={['back', 'demo']} loadDemoBudget={() => { - send('track', { name: 'app:create-demo' }); createBudget({ demoMode: true }); }} /> @@ -90,7 +89,4 @@ function Login({ navigation, createBudget }) { ); } -export default connect( - null, - actions -)(Login); +export default connect(null, actions)(Login); -- GitLab