From b307898b4758df9c66e0771abc53ce710c671728 Mon Sep 17 00:00:00 2001 From: Keshav <144454324+keshav0479@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:35:41 +0530 Subject: [PATCH] Mobile: Resolves #15853: Add note lock configuration section (#15963) Co-authored-by: mrjo118 --- .gitignore | 2 + .ignore.eslint | 2 + .../screens/ConfigScreen/ConfigScreen.tsx | 6 + .../ConfigScreen/NoteLockConfig.test.tsx | 51 ++++++ .../screens/ConfigScreen/NoteLockConfig.tsx | 153 ++++++++++++++++++ packages/lib/models/Setting.ts | 1 + 6 files changed, 215 insertions(+) create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.test.tsx create mode 100644 packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.tsx diff --git a/.gitignore b/.gitignore index 5b401fee58..71e3bd28f8 100644 --- a/.gitignore +++ b/.gitignore @@ -855,6 +855,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js +packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.test.js +packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.js packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js diff --git a/.ignore.eslint b/.ignore.eslint index c48470de3d..82291ff560 100644 --- a/.ignore.eslint +++ b/.ignore.eslint @@ -881,6 +881,8 @@ packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/expo packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportDebugReport.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/exportProfile.js packages/app-mobile/components/screens/ConfigScreen/NoteExportSection/utils/makeImportExportCacheDirectory.js +packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.test.js +packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.js packages/app-mobile/components/screens/ConfigScreen/SectionDescription.js packages/app-mobile/components/screens/ConfigScreen/SectionHeader.js packages/app-mobile/components/screens/ConfigScreen/SectionSelector/SectionTab.js diff --git a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx index aed8f91fff..57ce58de18 100644 --- a/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-mobile/components/screens/ConfigScreen/ConfigScreen.tsx @@ -36,6 +36,8 @@ import SectionDescription from './SectionDescription'; import EnablePluginSupportPage from './plugins/EnablePluginSupportPage'; import getVersionInfoText from '../../../utils/getVersionInfoText'; import JoplinCloudConfig, { emailToNoteDescription, emailToNoteLabel } from './JoplinCloudConfig'; +import NoteLockConfig from './NoteLockConfig'; +import isNoteLockEnabled from '@joplin/lib/services/noteLock/isNoteLockEnabled'; import shim from '@joplin/lib/shim'; import SettingsToggle from './SettingsToggle'; import { UpdateSettingValueCallback } from './types'; @@ -552,6 +554,10 @@ class ConfigScreenComponent extends BaseScreenComponent, [_('Password setup'), _('Note lock password')]); + } + if (section.name === 'joplinCloud') { addSettingComponent( ; +const WrappedNoteLockConfig: React.FC = () => { + return + + ; +}; + +describe('NoteLockConfig', () => { + beforeEach(async () => { + await setupDatabaseAndSynchronizer(0); + await switchClient(0); + NoteLockKey.destroyInstance(); + + store = createMockReduxStore(); + setupGlobalStore(store); + }); + afterEach(() => { + screen.unmount(); + }); + + test('should show the password setup form only while no note lock key is set', async () => { + const { unmount } = render(); + + expect(screen.getByText('Password setup')).toBeVisible(); + expect(screen.getByLabelText('Password')).toBeVisible(); + + // Seed the key via save() rather than create(): the mobile test crypto shim can't + // run real key generation. + act(() => { + NoteLockKey.instance().save({ id: 'test-note-lock-key' } as MasterKeyEntity); + }); + + expect(screen.queryByText('Password setup')).toBeNull(); + expect(screen.queryByLabelText('Password')).toBeNull(); + expect(screen.getByText(/Note lock password:.*Set/)).toBeVisible(); + + unmount(); + }); +}); diff --git a/packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.tsx b/packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.tsx new file mode 100644 index 0000000000..80ab16a52f --- /dev/null +++ b/packages/app-mobile/components/screens/ConfigScreen/NoteLockConfig.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { View, Text, TextInput, Button, StyleSheet } from 'react-native'; +import { connect } from 'react-redux'; +import { themeStyle } from '../../global-style'; +import { _ } from '@joplin/lib/locale'; +import NoteLockKey from '@joplin/lib/services/noteLock/NoteLockKey'; +import { SyncInfo } from '@joplin/lib/services/synchronizer/syncInfoUtils'; +import { State } from '@joplin/lib/reducer'; + +interface Props { + themeId: number; + hasNoteLockKey: boolean; +} + +const NoteLockConfig = (props: Props) => { + const [password, setPassword] = useState(''); + const [passwordRepeat, setPasswordRepeat] = useState(''); + const [error, setError] = useState(''); + const [saving, setSaving] = useState(false); + + const theme = useMemo(() => themeStyle(props.themeId), [props.themeId]); + + const styles = useMemo(() => { + return StyleSheet.create({ + container: { + padding: theme.margin, + }, + titleText: { + fontWeight: 'bold', + fontSize: theme.fontSize, + marginTop: theme.marginTop, + marginBottom: 5, + color: theme.color, + }, + normalText: { + fontSize: theme.fontSize, + color: theme.color, + marginBottom: 5, + }, + boldText: { + fontWeight: 'bold', + }, + passwordInput: { + marginTop: 5, + marginBottom: theme.itemMarginBottom, + color: theme.color, + borderWidth: 1, + borderColor: theme.dividerColor, + borderRadius: 3, + padding: 10, + }, + errorText: { + fontSize: theme.fontSize, + color: theme.colorError, + marginBottom: theme.itemMarginBottom, + }, + reminderText: { + fontSize: theme.fontSize, + fontWeight: 'bold', + color: theme.color, + marginTop: theme.itemMarginTop, + }, + }); + }, [theme]); + + const hasKey = props.hasNoteLockKey; + const passwordMismatch = !!passwordRepeat && password !== passwordRepeat; + const canSave = !!password && !!passwordRepeat && !passwordMismatch && !saving; + + const onCreate = useCallback(async () => { + if (!canSave) return; + setSaving(true); + setError(''); + try { + await NoteLockKey.instance().create(password); + setPassword(''); + setPasswordRepeat(''); + } catch (error) { + setError(error instanceof Error ? error.message : String(error)); + } finally { + setSaving(false); + } + }, [canSave, password]); + + const onPasswordChange = useCallback((value: string) => { + setPassword(value); + setError(''); + }, []); + + const onPasswordRepeatChange = useCallback((value: string) => { + setPasswordRepeat(value); + setError(''); + }, []); + + const passwordLabelId = 'note-lock-password'; + const repeatLabelId = 'note-lock-password-repeat'; + + const renderPasswordSetup = () => { + if (hasKey) return null; + return ( + + {_('Password setup')} + {_('Password')} + + {_('Repeat password')} + + {passwordMismatch ? {_('Passwords do not match')} : null} + {error ? {error} : null} +