From 88248e39ec0cad464b53d1a2581c2f0fefd39e1b Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 25 Jun 2026 15:03:19 -0300 Subject: [PATCH 1/4] refactor: migrate ScreenLockConfigView to a function component Rewrite ScreenLockConfigView from a class component to a function component with hooks, part of the Migrate to Hooks effort. - Replace connect/withTheme with useAppSelector and useTheme; init runs in a mount effect; the title moves to a useLayoutEffect setOptions. - Convert the setState(updater, callback) toggles to compute the next value explicitly and run side effects (DB save, biometry read) on that value, avoiding stale-state reads after a setter. - Undo a cancelled passcode setup inline (set off + save off) instead of recursively re-toggling, firing the toggle analytics event once. - Remove the dead `observable` field (declared and unsubscribed but never assigned; no real subscription was dropped). - Register the screen via the bare static-config pattern in InsideStack and MasterDetailStack, matching DisplayPrefsView. - Add ScreenLockConfigView.test.tsx covering render, the stale-state persist guard, passcode-cancel undo, biometry toggle, and auto-lock time change. Claude-Session: https://claude.ai/code/session_01PA87xg21JfSiVH6cZbeceC --- app/stacks/InsideStack.tsx | 9 +- app/stacks/MasterDetailStack/index.tsx | 8 +- app/views/ScreenLockConfigView.test.tsx | 162 +++++++++++ app/views/ScreenLockConfigView.tsx | 350 ++++++++++-------------- 4 files changed, 315 insertions(+), 214 deletions(-) create mode 100644 app/views/ScreenLockConfigView.test.tsx diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index 664190e602c..b28e61aee49 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -98,9 +98,7 @@ const ProfileViewScreen: ComponentType> = withNavig const ChangePasswordViewScreen: ComponentType> = withNavigation(ChangePasswordView as any) as any; const UserPreferencesViewScreen: ComponentType> = withNavigation(UserPreferencesView as any) as any; const SecurityPrivacyViewScreen: ComponentType> = withNavigation(SecurityPrivacyView as any) as any; -const ScreenLockConfigViewScreen: ComponentType> = withNavigation( - ScreenLockConfigView as any -) as any; +const ScreenLockConfigViewScreen: ComponentType> = ScreenLockConfigView as any; const CreateDiscussionViewScreen = withNavigation(CreateDiscussionView as any) as any; const E2EEnterYourPasswordViewScreen: ComponentType> = withNavigation( E2EEnterYourPasswordView as any @@ -239,10 +237,7 @@ const SettingsStack = createNativeStackNavigator({ MediaAutoDownloadView: MediaAutoDownloadViewScreen, GetHelpView: GetHelpViewScreen, LegalView: LegalViewScreen, - ScreenLockConfigView: createNativeStackScreen({ - screen: ScreenLockConfigViewScreen, - options: (): NativeStackNavigationOptions => (ScreenLockConfigView as any).navigationOptions() - }) + ScreenLockConfigView: ScreenLockConfigViewScreen } }).with(({ Navigator }) => { 'use memo'; diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index a4e517d2048..b368228a5c2 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -104,9 +104,6 @@ const TeamChannelsViewScreen: ComponentType> = withNavigation( ReadReceiptsView as any ) as any; -const ScreenLockConfigViewScreen: ComponentType> = withNavigation( - ScreenLockConfigView as any -) as any; const RoomInfoEditViewScreen: ComponentType> = withNavigation( RoomInfoEditView as any @@ -225,10 +222,7 @@ const ModalStack = createNativeStackNavigator({ LanguageView, ThemeView, DefaultBrowserView, - ScreenLockConfigView: createNativeStackScreen({ - screen: ScreenLockConfigViewScreen, - options: ScreenLockConfigView.navigationOptions - }), + ScreenLockConfigView, StatusView, ProfileView: ProfileViewScreen, ChangePasswordView: ChangePasswordViewScreen, diff --git a/app/views/ScreenLockConfigView.test.tsx b/app/views/ScreenLockConfigView.test.tsx new file mode 100644 index 00000000000..f3638cf976c --- /dev/null +++ b/app/views/ScreenLockConfigView.test.tsx @@ -0,0 +1,162 @@ +import { fireEvent, render, waitFor } from '@testing-library/react-native'; + +import ScreenLockConfigView from './ScreenLockConfigView'; +import { BIOMETRY_ENABLED_KEY, DEFAULT_AUTO_LOCK } from '../lib/constants/localAuthentication'; + +const mockSetOptions = jest.fn(); + +jest.mock('@react-navigation/native', () => ({ + useNavigation: () => ({ setOptions: mockSetOptions }) +})); + +jest.mock('../theme', () => ({ + useTheme: () => ({ theme: 'light', colors: {} }) +})); + +jest.mock('../lib/hooks/useAppSelector', () => ({ + useAppSelector: (selector: (state: any) => unknown) => + selector({ + server: { server: 'srv' }, + settings: { Force_Screen_Lock: false, Force_Screen_Lock_After: 0 } + }) +})); + +const update = jest.fn(cb => cb({ autoLock: false, autoLockTime: null })); +const fakeRecord = { autoLock: false, autoLockTime: null, update }; + +const mockWrite = jest.fn(fn => fn()); +const mockFind = jest.fn(() => Promise.resolve(fakeRecord)); + +jest.mock('../lib/database', () => ({ + __esModule: true, + default: { + servers: { + get: () => ({ find: mockFind }), + write: (fn: () => Promise) => mockWrite(fn) + } + } +})); + +const mockSupportedBiometryLabel = jest.fn(() => Promise.resolve('Face ID')); +const mockCheckHasPasscode = jest.fn(); + +jest.mock('../lib/methods/helpers/localAuthentication', () => ({ + supportedBiometryLabel: () => mockSupportedBiometryLabel(), + checkHasPasscode: (args: any) => mockCheckHasPasscode(args), + changePasscode: jest.fn(), + handleLocalAuthentication: jest.fn() +})); + +const mockGetBool = jest.fn((_key: string) => false as boolean | null); +const mockSetBool = jest.fn((_key: string, _value: boolean) => {}); + +jest.mock('../lib/methods/userPreferences', () => ({ + __esModule: true, + default: { + getBool: (key: string) => mockGetBool(key), + setBool: (key: string, value: boolean) => mockSetBool(key, value) + } +})); + +jest.mock('../lib/methods/helpers/log', () => ({ + events: { + SLC_SAVE_SCREEN_LOCK: 'SLC_SAVE_SCREEN_LOCK', + SLC_TOGGLE_AUTOLOCK: 'SLC_TOGGLE_AUTOLOCK', + SLC_TOGGLE_BIOMETRY: 'SLC_TOGGLE_BIOMETRY', + SLC_CHANGE_AUTOLOCK_TIME: 'SLC_CHANGE_AUTOLOCK_TIME', + SLC_CHANGE_PASSCODE: 'SLC_CHANGE_PASSCODE' + }, + logEvent: jest.fn() +})); + +jest.mock('../containers/SafeAreaView', () => { + const { View } = require('react-native'); + return ({ children }: any) => {children}; +}); + +describe('ScreenLockConfigView', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFind.mockResolvedValue(fakeRecord); + mockSupportedBiometryLabel.mockResolvedValue('Face ID'); + mockGetBool.mockReturnValue(false); + }); + + it('renders the auto-lock list item after init', async () => { + const { findByText } = render(); + + await findByText('Unlock with passcode'); + }); + + it('enable auto-lock persists true — stale-state guard', async () => { + mockCheckHasPasscode.mockResolvedValue(undefined); + + const { findByTestId } = render(); + const autoLockSwitch = await findByTestId('screen-lock-config-auto-lock-switch'); + fireEvent(autoLockSwitch, 'onValueChange', true); + + await waitFor(() => expect(mockWrite).toHaveBeenCalled()); + + expect(update).toHaveBeenCalled(); + const recordArg: any = {}; + const cb = update.mock.calls[update.mock.calls.length - 1][0]; + cb(recordArg); + expect(recordArg.autoLock).toBe(true); + expect(recordArg.autoLockTime).toBe(DEFAULT_AUTO_LOCK); + }); + + it('passcode-cancel undo — ends with autoLock false persisted', async () => { + mockCheckHasPasscode.mockRejectedValue(new Error('cancelled')); + + const { findByTestId } = render(); + const autoLockSwitch = await findByTestId('screen-lock-config-auto-lock-switch'); + fireEvent(autoLockSwitch, 'onValueChange', true); + + await waitFor(() => expect(mockWrite).toHaveBeenCalled()); + + expect(update).toHaveBeenCalled(); + const recordArg: any = {}; + const cb = update.mock.calls[update.mock.calls.length - 1][0]; + cb(recordArg); + expect(recordArg.autoLock).toBe(false); + }); + + it('toggle biometry calls userPreferences.setBool with flipped value', async () => { + mockCheckHasPasscode.mockResolvedValue(undefined); + + const { findByTestId } = render(); + const autoLockSwitch = await findByTestId('screen-lock-config-auto-lock-switch'); + fireEvent(autoLockSwitch, 'onValueChange', true); + await waitFor(() => expect(mockWrite).toHaveBeenCalled()); + + mockWrite.mockClear(); + + const biometrySwitch = await findByTestId('screen-lock-config-biometry-switch'); + fireEvent(biometrySwitch, 'onValueChange', true); + + await waitFor(() => expect(mockSetBool).toHaveBeenCalledWith(BIOMETRY_ENABLED_KEY, true)); + }); + + it('change auto-lock time persists selected value with current autoLock', async () => { + mockCheckHasPasscode.mockResolvedValue(undefined); + + const { findByTestId, findByText } = render(); + const autoLockSwitch = await findByTestId('screen-lock-config-auto-lock-switch'); + fireEvent(autoLockSwitch, 'onValueChange', true); + await waitFor(() => expect(mockWrite).toHaveBeenCalled()); + + mockWrite.mockClear(); + update.mockClear(); + + const timeOption = await findByText('After 1 minute'); + fireEvent.press(timeOption.parent!); + + await waitFor(() => expect(mockWrite).toHaveBeenCalled()); + + expect(update).toHaveBeenCalled(); + const recordArg: any = {}; + const cb = update.mock.calls[update.mock.calls.length - 1][0]; + cb(recordArg); + expect(recordArg.autoLockTime).toBe(60); + }); +}); diff --git a/app/views/ScreenLockConfigView.tsx b/app/views/ScreenLockConfigView.tsx index dc5f2f92d4c..dac5881fb8a 100644 --- a/app/views/ScreenLockConfigView.tsx +++ b/app/views/ScreenLockConfigView.tsx @@ -1,9 +1,9 @@ -import { connect } from 'react-redux'; -import { type Subscription } from 'rxjs'; -import { Component } from 'react'; +import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'; +import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { useNavigation } from '@react-navigation/native'; import I18n from '../i18n'; -import { type TSupportedThemes, withTheme } from '../theme'; +import { useTheme } from '../theme'; import * as List from '../containers/List'; import database from '../lib/database'; import { @@ -13,12 +13,13 @@ import { handleLocalAuthentication } from '../lib/methods/helpers/localAuthentication'; import { BIOMETRY_ENABLED_KEY, DEFAULT_AUTO_LOCK } from '../lib/constants/localAuthentication'; -import { themes } from '../lib/constants/colors'; import SafeAreaView from '../containers/SafeAreaView'; import { events, logEvent } from '../lib/methods/helpers/log'; import userPreferences from '../lib/methods/userPreferences'; -import { type IApplicationState, type TServerModel } from '../definitions'; +import { type TServerModel } from '../definitions'; import Switch from '../containers/Switch'; +import { type SettingsStackParamList } from '../stacks/types'; +import { useAppSelector } from '../lib/hooks/useAppSelector'; const DEFAULT_BIOMETRY = false; @@ -28,172 +29,132 @@ interface IItem { disabled?: boolean; } -interface IScreenLockConfigViewProps { - theme: TSupportedThemes; - server: string; - Force_Screen_Lock: boolean; - Force_Screen_Lock_After: number; -} - -interface IScreenLockConfigViewState { - autoLock: boolean; - autoLockTime?: number | null; - biometry: boolean; - biometryLabel: string | null; -} - -class ScreenLockConfigView extends Component { - private serverRecord?: TServerModel; - - private observable?: Subscription; - - static navigationOptions = () => ({ - title: I18n.t('Screen_lock') - }); - - constructor(props: IScreenLockConfigViewProps) { - super(props); - this.state = { - autoLock: false, - autoLockTime: null, - biometry: DEFAULT_BIOMETRY, - biometryLabel: null - }; - this.init(); - } - - componentWillUnmount() { - if (this.observable && this.observable.unsubscribe) { - this.observable.unsubscribe(); - } +const defaultAutoLockOptions: IItem[] = [ + { + title: I18n.t('Local_authentication_auto_lock_60'), + value: 60 + }, + { + title: I18n.t('Local_authentication_auto_lock_300'), + value: 300 + }, + { + title: I18n.t('Local_authentication_auto_lock_900'), + value: 900 + }, + { + title: I18n.t('Local_authentication_auto_lock_1800'), + value: 1800 + }, + { + title: I18n.t('Local_authentication_auto_lock_3600'), + value: 3600 } +]; + +const ScreenLockConfigView = (): ReactElement => { + const navigation = useNavigation>(); + const { colors } = useTheme(); + + const server = useAppSelector(state => state.server.server); + const Force_Screen_Lock = useAppSelector(state => state.settings.Force_Screen_Lock as boolean); + const Force_Screen_Lock_After = useAppSelector(state => state.settings.Force_Screen_Lock_After as number); + + const serverRecord = useRef(undefined); + + const [autoLock, setAutoLock] = useState(false); + const [autoLockTime, setAutoLockTime] = useState(null); + const [biometry, setBiometry] = useState(DEFAULT_BIOMETRY); + const [biometryLabel, setBiometryLabel] = useState(null); + + useLayoutEffect(() => { + navigation.setOptions({ title: I18n.t('Screen_lock') }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + const init = async () => { + try { + serverRecord.current = await database.servers.get('servers').find(server); + setAutoLock(serverRecord.current?.autoLock ?? false); + setAutoLockTime( + serverRecord.current?.autoLockTime === null ? DEFAULT_AUTO_LOCK : serverRecord.current?.autoLockTime ?? null + ); + setBiometry(userPreferences.getBool(BIOMETRY_ENABLED_KEY) ?? DEFAULT_BIOMETRY); + } catch { + // noop + } + setBiometryLabel(await supportedBiometryLabel()); + }; + init(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - defaultAutoLockOptions = [ - { - title: I18n.t('Local_authentication_auto_lock_60'), - value: 60 - }, - { - title: I18n.t('Local_authentication_auto_lock_300'), - value: 300 - }, - { - title: I18n.t('Local_authentication_auto_lock_900'), - value: 900 - }, - { - title: I18n.t('Local_authentication_auto_lock_1800'), - value: 1800 - }, - { - title: I18n.t('Local_authentication_auto_lock_3600'), - value: 3600 - } - ]; - - init = async () => { - const { server } = this.props; - const serversDB = database.servers; - const serversCollection = serversDB.get('servers'); - try { - this.serverRecord = await serversCollection.find(server); - this.setState( - { - autoLock: this.serverRecord?.autoLock, - autoLockTime: this.serverRecord?.autoLockTime === null ? DEFAULT_AUTO_LOCK : this.serverRecord?.autoLockTime - }, - () => this.hasBiometry() - ); - } catch (error) { - // Do nothing - } - - const biometryLabel = await supportedBiometryLabel(); - this.setState({ biometryLabel }); - }; - - save = async () => { + const save = async (nextAutoLock: boolean, nextAutoLockTime: number | null) => { logEvent(events.SLC_SAVE_SCREEN_LOCK); - const { autoLock, autoLockTime } = this.state; - const serversDB = database.servers; - await serversDB.write(async () => { - await this.serverRecord?.update(record => { - record.autoLock = autoLock; - record.autoLockTime = autoLockTime === null ? DEFAULT_AUTO_LOCK : autoLockTime; + await database.servers.write(async () => { + await serverRecord.current?.update(record => { + record.autoLock = nextAutoLock; + record.autoLockTime = nextAutoLockTime === null ? DEFAULT_AUTO_LOCK : nextAutoLockTime; }); }); }; - hasBiometry = () => { - const biometry = userPreferences.getBool(BIOMETRY_ENABLED_KEY) ?? DEFAULT_BIOMETRY; - this.setState({ biometry }); - }; - - changePasscode = async ({ force }: { force: boolean }) => { - const { autoLock } = this.state; - if (autoLock) { - await handleLocalAuthentication(true); - } - logEvent(events.SLC_CHANGE_PASSCODE); - await changePasscode({ force }); - }; - - toggleAutoLock = () => { + const toggleAutoLock = async () => { logEvent(events.SLC_TOGGLE_AUTOLOCK); - this.setState( - ({ autoLock }) => ({ autoLock: !autoLock, autoLockTime: DEFAULT_AUTO_LOCK }), - async () => { - const { autoLock } = this.state; - if (autoLock) { - try { - await checkHasPasscode({ force: false }); - this.hasBiometry(); - } catch { - this.toggleAutoLock(); - } - } - this.save(); + const nextAutoLock = !autoLock; + setAutoLock(nextAutoLock); + setAutoLockTime(DEFAULT_AUTO_LOCK); + if (nextAutoLock) { + try { + await checkHasPasscode({ force: false }); + setBiometry(userPreferences.getBool(BIOMETRY_ENABLED_KEY) ?? DEFAULT_BIOMETRY); + } catch { + setAutoLock(false); + setAutoLockTime(DEFAULT_AUTO_LOCK); + await save(false, DEFAULT_AUTO_LOCK); + return; } - ); + } + await save(nextAutoLock, DEFAULT_AUTO_LOCK); }; - toggleBiometry = () => { + const toggleBiometry = () => { logEvent(events.SLC_TOGGLE_BIOMETRY); - this.setState( - ({ biometry }) => ({ biometry: !biometry }), - () => { - const { biometry } = this.state; - userPreferences.setBool(BIOMETRY_ENABLED_KEY, biometry); - } - ); + const nextBiometry = !biometry; + setBiometry(nextBiometry); + userPreferences.setBool(BIOMETRY_ENABLED_KEY, nextBiometry); }; - isSelected = (value: number) => { - const { autoLockTime } = this.state; - return autoLockTime === value; - }; + const isSelected = (value: number) => autoLockTime === value; - changeAutoLockTime = (autoLockTime: number) => { + const changeAutoLockTime = (nextAutoLockTime: number) => { logEvent(events.SLC_CHANGE_AUTOLOCK_TIME); - this.setState({ autoLockTime }, () => this.save()); + setAutoLockTime(nextAutoLockTime); + save(autoLock, nextAutoLockTime); }; - renderIcon = () => { - const { theme } = this.props; - return ; + const handleChangePasscode = async ({ force }: { force: boolean }) => { + if (autoLock) { + await handleLocalAuthentication(true); + } + logEvent(events.SLC_CHANGE_PASSCODE); + await changePasscode({ force }); }; - renderItem = ({ item }: { item: IItem }) => { + const renderIcon = () => ; + + const renderItem = ({ item }: { item: IItem }) => { const { title, value, disabled } = item; return ( <> this.changeAutoLockTime(value)} - right={() => (this.isSelected(value) ? this.renderIcon() : null)} + onPress={() => changeAutoLockTime(value)} + right={() => (isSelected(value) ? renderIcon() : null)} disabled={disabled} translateTitle={false} - additionalAccessibilityLabel={this.isSelected(value)} + additionalAccessibilityLabel={isSelected(value)} additionalAccessibilityLabelCheck /> @@ -201,24 +162,24 @@ class ScreenLockConfigView extends Component { - const { autoLock } = this.state; - const { Force_Screen_Lock } = this.props; - return ; - }; + const renderAutoLockSwitch = () => ( + + ); - renderBiometrySwitch = () => { - const { biometry } = this.state; - return ; - }; + const renderBiometrySwitch = () => ( + + ); - renderAutoLockItems = () => { - const { autoLock, autoLockTime } = this.state; - const { Force_Screen_Lock_After, Force_Screen_Lock } = this.props; + const renderAutoLockItems = () => { if (!autoLock) { return null; } - let items: IItem[] = this.defaultAutoLockOptions; + let items: IItem[] = [...defaultAutoLockOptions]; if (Force_Screen_Lock && Force_Screen_Lock_After > 0) { items = [ { @@ -227,7 +188,6 @@ class ScreenLockConfigView extends Component item.value === autoLockTime)) { items.push({ title: I18n.t('After_seconds_set_by_admin', { seconds: Force_Screen_Lock_After }), @@ -237,13 +197,12 @@ class ScreenLockConfigView extends Component - <>{items.map(item => this.renderItem({ item }))} + <>{items.map(item => renderItem({ item }))} ); }; - renderBiometry = () => { - const { autoLock, biometryLabel } = this.state; + const renderBiometry = () => { if (!autoLock || !biometryLabel) { return null; } @@ -252,48 +211,39 @@ class ScreenLockConfigView extends Component this.renderBiometrySwitch()} + right={() => renderBiometrySwitch()} translateTitle={false} - additionalAccessibilityLabel={this.state.biometry ? I18n.t('Enabled') : I18n.t('Disabled')} + additionalAccessibilityLabel={biometry ? I18n.t('Enabled') : I18n.t('Disabled')} /> ); }; - render() { - const { autoLock } = this.state; - return ( - - - - - this.renderAutoLockSwitch()} - additionalAccessibilityLabel={autoLock} - /> - {autoLock ? ( - <> - - - - ) : null} - - - - {this.renderBiometry()} - {this.renderAutoLockItems()} - - - ); - } -} - -const mapStateToProps = (state: IApplicationState) => ({ - server: state.server.server, - Force_Screen_Lock: state.settings.Force_Screen_Lock as boolean, - Force_Screen_Lock_After: state.settings.Force_Screen_Lock_After as number -}); - -export default connect(mapStateToProps)(withTheme(ScreenLockConfigView)); + return ( + + + + + renderAutoLockSwitch()} + additionalAccessibilityLabel={autoLock} + /> + {autoLock ? ( + <> + + + + ) : null} + + + + {renderBiometry()} + {renderAutoLockItems()} + + + ); +}; + +export default ScreenLockConfigView; From bb2c3cd0f2f06b5e1d8f1816b244892beb6be5d0 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 25 Jun 2026 15:13:32 -0300 Subject: [PATCH 2/4] refactor: use explicit effect deps instead of exhaustive-deps disables Address review on ScreenLockConfigView: list the stable navigation and server dependencies on the title and init effects instead of suppressing the react-hooks/exhaustive-deps rule. Both values are referentially stable, so each effect still runs once on mount; no behavior change. Claude-Session: https://claude.ai/code/session_01PA87xg21JfSiVH6cZbeceC --- app/views/ScreenLockConfigView.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/views/ScreenLockConfigView.tsx b/app/views/ScreenLockConfigView.tsx index dac5881fb8a..24b7fef26a7 100644 --- a/app/views/ScreenLockConfigView.tsx +++ b/app/views/ScreenLockConfigView.tsx @@ -69,8 +69,7 @@ const ScreenLockConfigView = (): ReactElement => { useLayoutEffect(() => { navigation.setOptions({ title: I18n.t('Screen_lock') }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [navigation]); useEffect(() => { const init = async () => { @@ -87,8 +86,7 @@ const ScreenLockConfigView = (): ReactElement => { setBiometryLabel(await supportedBiometryLabel()); }; init(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [server]); const save = async (nextAutoLock: boolean, nextAutoLockTime: number | null) => { logEvent(events.SLC_SAVE_SCREEN_LOCK); From 76539240897be48f016cb24307b1a4e296448480 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 25 Jun 2026 15:30:06 -0300 Subject: [PATCH 3/4] refactor: address review feedback on ScreenLockConfigView migration - key the mapped auto-lock duration rows with a stable Fragment key - assert rolled-back autoLockTime in the passcode-cancel test - drop unnecessary `as any` from the screen registration Claude-Session: https://claude.ai/code/session_01PA87xg21JfSiVH6cZbeceC --- app/stacks/InsideStack.tsx | 2 +- app/views/ScreenLockConfigView.test.tsx | 1 + app/views/ScreenLockConfigView.tsx | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index b28e61aee49..2a455d6751d 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -98,7 +98,7 @@ const ProfileViewScreen: ComponentType> = withNavig const ChangePasswordViewScreen: ComponentType> = withNavigation(ChangePasswordView as any) as any; const UserPreferencesViewScreen: ComponentType> = withNavigation(UserPreferencesView as any) as any; const SecurityPrivacyViewScreen: ComponentType> = withNavigation(SecurityPrivacyView as any) as any; -const ScreenLockConfigViewScreen: ComponentType> = ScreenLockConfigView as any; +const ScreenLockConfigViewScreen: ComponentType> = ScreenLockConfigView; const CreateDiscussionViewScreen = withNavigation(CreateDiscussionView as any) as any; const E2EEnterYourPasswordViewScreen: ComponentType> = withNavigation( E2EEnterYourPasswordView as any diff --git a/app/views/ScreenLockConfigView.test.tsx b/app/views/ScreenLockConfigView.test.tsx index f3638cf976c..61dce2dbdb9 100644 --- a/app/views/ScreenLockConfigView.test.tsx +++ b/app/views/ScreenLockConfigView.test.tsx @@ -119,6 +119,7 @@ describe('ScreenLockConfigView', () => { const cb = update.mock.calls[update.mock.calls.length - 1][0]; cb(recordArg); expect(recordArg.autoLock).toBe(false); + expect(recordArg.autoLockTime).toBe(DEFAULT_AUTO_LOCK); }); it('toggle biometry calls userPreferences.setBool with flipped value', async () => { diff --git a/app/views/ScreenLockConfigView.tsx b/app/views/ScreenLockConfigView.tsx index 24b7fef26a7..a0369f53d6b 100644 --- a/app/views/ScreenLockConfigView.tsx +++ b/app/views/ScreenLockConfigView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'; +import { Fragment, useEffect, useLayoutEffect, useRef, useState, type ReactElement } from 'react'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useNavigation } from '@react-navigation/native'; @@ -145,7 +145,7 @@ const ScreenLockConfigView = (): ReactElement => { const renderItem = ({ item }: { item: IItem }) => { const { title, value, disabled } = item; return ( - <> + changeAutoLockTime(value)} @@ -156,7 +156,7 @@ const ScreenLockConfigView = (): ReactElement => { additionalAccessibilityLabelCheck /> - + ); }; From d9bbeb32a3e6669891303a0fd6c42f24e59114d6 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Thu, 25 Jun 2026 15:46:59 -0300 Subject: [PATCH 4/4] fix: re-evaluate auto-lock option labels on each render Move the auto-lock duration options out of module scope and build them inside renderAutoLockItems so I18n.t runs with the current language. At module scope the labels were translated once at import and went stale after a language change. Claude-Session: https://claude.ai/code/session_01PA87xg21JfSiVH6cZbeceC --- app/views/ScreenLockConfigView.tsx | 31 +++++++----------------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/app/views/ScreenLockConfigView.tsx b/app/views/ScreenLockConfigView.tsx index a0369f53d6b..a9310802ff6 100644 --- a/app/views/ScreenLockConfigView.tsx +++ b/app/views/ScreenLockConfigView.tsx @@ -29,29 +29,6 @@ interface IItem { disabled?: boolean; } -const defaultAutoLockOptions: IItem[] = [ - { - title: I18n.t('Local_authentication_auto_lock_60'), - value: 60 - }, - { - title: I18n.t('Local_authentication_auto_lock_300'), - value: 300 - }, - { - title: I18n.t('Local_authentication_auto_lock_900'), - value: 900 - }, - { - title: I18n.t('Local_authentication_auto_lock_1800'), - value: 1800 - }, - { - title: I18n.t('Local_authentication_auto_lock_3600'), - value: 3600 - } -]; - const ScreenLockConfigView = (): ReactElement => { const navigation = useNavigation>(); const { colors } = useTheme(); @@ -177,7 +154,13 @@ const ScreenLockConfigView = (): ReactElement => { if (!autoLock) { return null; } - let items: IItem[] = [...defaultAutoLockOptions]; + let items: IItem[] = [ + { title: I18n.t('Local_authentication_auto_lock_60'), value: 60 }, + { title: I18n.t('Local_authentication_auto_lock_300'), value: 300 }, + { title: I18n.t('Local_authentication_auto_lock_900'), value: 900 }, + { title: I18n.t('Local_authentication_auto_lock_1800'), value: 1800 }, + { title: I18n.t('Local_authentication_auto_lock_3600'), value: 3600 } + ]; if (Force_Screen_Lock && Force_Screen_Lock_After > 0) { items = [ {