1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-04-23 11:52:59 +02:00

Mobile: Resolves #11846: Improve encryption config screen accessibility (#11874)

This commit is contained in:
Henry Heino 2025-02-23 06:08:09 -08:00 committed by GitHub
parent 69b24b4437
commit 55a57f7baf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 139 additions and 30 deletions

View File

@ -787,6 +787,7 @@ packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/status.js packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js packages/app-mobile/components/screens/tags.js

1
.gitignore vendored
View File

@ -762,6 +762,7 @@ packages/app-mobile/components/screens/ShareManager/index.test.js
packages/app-mobile/components/screens/ShareManager/index.js packages/app-mobile/components/screens/ShareManager/index.js
packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js packages/app-mobile/components/screens/UpgradeSyncTargetScreen.js
packages/app-mobile/components/screens/dropbox-login.js packages/app-mobile/components/screens/dropbox-login.js
packages/app-mobile/components/screens/encryption-config.test.js
packages/app-mobile/components/screens/encryption-config.js packages/app-mobile/components/screens/encryption-config.js
packages/app-mobile/components/screens/status.js packages/app-mobile/components/screens/status.js
packages/app-mobile/components/screens/tags.js packages/app-mobile/components/screens/tags.js

View File

@ -0,0 +1,74 @@
import * as React from 'react';
import { Store } from 'redux';
import { AppState } from '../../utils/types';
import TestProviderStack from '../testing/TestProviderStack';
import EncryptionConfig from './encryption-config';
import { loadEncryptionMasterKey, setupDatabaseAndSynchronizer, switchClient, synchronizerStart } from '@joplin/lib/testing/test-utils';
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
import { getActiveMasterKeyId, setEncryptionEnabled, setMasterKeyEnabled } from '@joplin/lib/services/synchronizer/syncInfoUtils';
import { act, render, screen } from '@testing-library/react-native';
import '@testing-library/jest-native/extend-expect';
interface WrapperProps { }
let store: Store<AppState>;
const WrappedEncryptionConfigScreen: React.FC<WrapperProps> = _props => {
return <TestProviderStack store={store}>
<EncryptionConfig/>
</TestProviderStack>;
};
describe('encryption-config', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(0);
await switchClient(0);
setEncryptionEnabled(true);
await loadEncryptionMasterKey();
store = createMockReduxStore();
setupGlobalStore(store);
});
afterEach(() => {
screen.unmount();
});
test('should show an input for entering the master password after an initial sync', async () => {
// Switch to the other client and sync so that there's a master key missing
// a password
await synchronizerStart();
await switchClient(1);
await synchronizerStart();
const { unmount } = render(<WrappedEncryptionConfigScreen/>);
// Should auto-enable encryption
expect(screen.getByText('Encryption is: Enabled')).toBeVisible();
const passwordInput = screen.getByLabelText(/The master password is not set/);
expect(passwordInput).toBeVisible();
// Unmount here to prevent "An update to EncryptionConfigScreen inside a test was not wrapped in act(...)"
// errors
unmount();
});
test('should not show the "disabled keys" dropdown unless there are disabled keys', async () => {
const masterKeyId = getActiveMasterKeyId();
setMasterKeyEnabled(masterKeyId, false);
const { unmount } = render(<WrappedEncryptionConfigScreen/>);
const queryDisabledKeysButton = () => screen.queryByRole('button', { name: 'Disabled keys' });
// Should be visible when there are disabled keys
expect(queryDisabledKeysButton()).toBeVisible();
// Enabling the key should hide the button
act(() => setMasterKeyEnabled(masterKeyId, true));
expect(queryDisabledKeysButton()).toBeNull();
unmount();
});
});

View File

@ -1,6 +1,6 @@
const React = require('react'); import * as React from 'react';
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native'); import { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } from 'react-native';
const { connect } = require('react-redux'); import { connect } from 'react-redux';
import ScreenHeader from '../ScreenHeader'; import ScreenHeader from '../ScreenHeader';
import { themeStyle } from '../global-style'; import { themeStyle } from '../global-style';
import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService'; import EncryptionService from '@joplin/lib/services/e2ee/EncryptionService';
@ -16,13 +16,11 @@ import { Divider, List } from 'react-native-paper';
import shim from '@joplin/lib/shim'; import shim from '@joplin/lib/shim';
interface Props { interface Props {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied themeId: number;
themeId: any;
masterKeys: MasterKeyEntity[]; masterKeys: MasterKeyEntity[];
passwords: Record<string, string>; passwords: Record<string, string>;
notLoadedMasterKeys: string[]; notLoadedMasterKeys: string[];
encryptionEnabled: boolean; encryptionEnabled: boolean;
shouldReencrypt: boolean;
activeMasterKeyId: string; activeMasterKeyId: string;
masterPassword: string; masterPassword: string;
} }
@ -54,7 +52,7 @@ const EncryptionConfigScreen = (props: Props) => {
}, [theme]); }, [theme]);
const styles = useMemo(() => { const styles = useMemo(() => {
const styles = { return StyleSheet.create({
titleText: { titleText: {
flex: 1, flex: 1,
fontWeight: 'bold', fontWeight: 'bold',
@ -85,9 +83,7 @@ const EncryptionConfigScreen = (props: Props) => {
paddingLeft: theme.margin, paddingLeft: theme.margin,
paddingRight: theme.margin, paddingRight: theme.margin,
}, },
}; });
return StyleSheet.create(styles);
}, [theme]); }, [theme]);
const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null; const decryptedItemsInfo = props.encryptionEnabled ? <Text style={styles.normalText}>{decryptedStatText(stats)}</Text> : null;
@ -96,7 +92,8 @@ const EncryptionConfigScreen = (props: Props) => {
const theme = themeStyle(props.themeId); const theme = themeStyle(props.themeId);
const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : ''; const password = inputPasswords[mk.id] ? inputPasswords[mk.id] : '';
const passwordOk = passwordChecks[mk.id] === true ? '✔' : '❌'; const passwordOk = passwordChecks[mk.id] === true;
const passwordOkIcon = passwordOk ? '✔' : '❌';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color }; const inputStyle: any = { flex: 1, marginRight: 10, color: theme.color };
@ -111,8 +108,19 @@ const EncryptionConfigScreen = (props: Props) => {
} else { } else {
return ( return (
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}> <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={password} onChangeText={(text: string) => onInputPasswordChange(mk, text)} style={inputStyle}></TextInput> <TextInput
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text> selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
secureTextEntry={true}
value={password}
onChangeText={(text: string) => onInputPasswordChange(mk, text)}
style={inputStyle}
/>
<Text
style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}
accessibilityRole='image'
accessibilityLabel={passwordOk ? _('Valid') : _('Invalid password')}
>{passwordOkIcon}</Text>
<Button title={_('Save')} onPress={() => onSavePasswordClick(mk, inputPasswords)}></Button> <Button title={_('Save')} onPress={() => onSavePasswordClick(mk, inputPasswords)}></Button>
</View> </View>
); );
@ -121,7 +129,10 @@ const EncryptionConfigScreen = (props: Props) => {
return ( return (
<View key={mk.id}> <View key={mk.id}>
<Text style={styles.titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text> <Text
style={styles.titleText}
accessibilityRole='header'
>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
<Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text> <Text style={styles.normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text> <Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
@ -157,11 +168,14 @@ const EncryptionConfigScreen = (props: Props) => {
return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>; return <Text key={msg} style={{ fontSize: theme.fontSize, color: theme.color, marginBottom: 10 }}>{msg}</Text>;
}); });
const passwordLabelId = 'password-label';
const confirmPasswordLabelId = 'confirm-password';
return ( return (
<View style={{ flex: 1, flexBasis: 'auto', borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}> <View style={{ flex: 1, flexBasis: 'auto', borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
<View>{messageComps}</View> <View>{messageComps}</View>
<Text style={styles.normalText}>{_('Password:')}</Text> <Text nativeID={passwordLabelId} style={styles.normalText}>{_('Password:')}</Text>
<TextInput <TextInput
accessibilityLabelledBy={passwordLabelId}
selectionColor={theme.textSelectionColor} selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance} keyboardAppearance={theme.keyboardAppearance}
style={styles.normalTextInput} style={styles.normalTextInput}
@ -172,8 +186,9 @@ const EncryptionConfigScreen = (props: Props) => {
}} }}
></TextInput> ></TextInput>
<Text style={styles.normalText}>{_('Confirm password:')}</Text> <Text nativeID={confirmPasswordLabelId} style={styles.normalText}>{_('Confirm password:')}</Text>
<TextInput <TextInput
accessibilityLabelledBy={confirmPasswordLabelId}
selectionColor={theme.textSelectionColor} selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance} keyboardAppearance={theme.keyboardAppearance}
style={styles.normalTextInput} style={styles.normalTextInput}
@ -221,11 +236,23 @@ const EncryptionConfigScreen = (props: Props) => {
</View> </View>
); );
} else { } else {
const labelId = 'master-password-label';
return ( return (
<View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}> <View style={{ display: 'flex', flexDirection: 'column', marginTop: 10 }}>
<Text style={styles.normalText}>{'The master password is not set or is invalid. Please type it below:'}</Text> <Text
style={styles.normalText}
nativeID={labelId}
>{'The master password is not set or is invalid. Please type it below:'}</Text>
<View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}> <View style={{ display: 'flex', flexDirection: 'row', marginTop: 10 }}>
<TextInput selectionColor={theme.textSelectionColor} keyboardAppearance={theme.keyboardAppearance} secureTextEntry={true} value={inputMasterPassword} onChangeText={(text: string) => onMasterPasswordChange(text)} style={inputStyle}></TextInput> <TextInput
accessibilityLabelledBy={labelId}
selectionColor={theme.textSelectionColor}
keyboardAppearance={theme.keyboardAppearance}
secureTextEntry={true}
value={inputMasterPassword}
onChangeText={(text: string) => onMasterPasswordChange(text)}
style={inputStyle}
/>
<Button onPress={onMasterPasswordSave} title={_('Save')} /> <Button onPress={onMasterPasswordSave} title={_('Save')} />
</View> </View>
</View> </View>
@ -293,6 +320,17 @@ const EncryptionConfigScreen = (props: Props) => {
</View> </View>
) : null; ) : null;
const disabledMasterKeyList = disabledMkComps.length ? <List.Accordion
title={_('Disabled keys')}
titleStyle={styles.titleText}
expanded={showDisabledKeys}
onPress={() => setShowDisabledKeys(st => !st)}
>
<View style={styles.disabledContainer}>
{disabledMkComps}
</View>
</List.Accordion> : null;
return ( return (
<View style={rootStyle}> <View style={rootStyle}>
<ScreenHeader title={_('Encryption Config')} /> <ScreenHeader title={_('Encryption Config')} />
@ -302,14 +340,18 @@ const EncryptionConfigScreen = (props: Props) => {
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text> <Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
Linking.openURL('https://joplinapp.org/help/apps/sync/e2ee'); void Linking.openURL('https://joplinapp.org/help/apps/sync/e2ee');
}} }}
accessibilityRole='link'
> >
<Text>https://joplinapp.org/help/apps/sync/e2ee</Text> <Text>https://joplinapp.org/help/apps/sync/e2ee</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<Text style={styles.titleText}>{_('Status')}</Text> <Text
style={styles.titleText}
accessibilityRole='header'
>{_('Status')}</Text>
<Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text> <Text style={styles.normalText}>{_('Encryption is: %s', props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
{decryptedItemsInfo} {decryptedItemsInfo}
{renderMasterPassword()} {renderMasterPassword()}
@ -319,16 +361,7 @@ const EncryptionConfigScreen = (props: Props) => {
{nonExistingMasterKeySection} {nonExistingMasterKeySection}
</View> </View>
<Divider /> <Divider />
<List.Accordion {disabledMasterKeyList}
title={_('Disabled keys')}
titleStyle={styles.titleText}
expanded={showDisabledKeys}
onPress={() => setShowDisabledKeys(st => !st)}
>
<View style={styles.disabledContainer}>
{disabledMkComps}
</View>
</List.Accordion>
</ScrollView> </ScrollView>
</View> </View>
); );