diff --git a/components/pages/SettingsScreen.tsx b/components/pages/SettingsScreen.tsx index de2966e..3f37255 100644 --- a/components/pages/SettingsScreen.tsx +++ b/components/pages/SettingsScreen.tsx @@ -1,7 +1,8 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useRef, useEffect } from 'react'; import { View, Text, TouchableOpacity, ScrollView, Animated } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; +import { useSettingsScreen, PrefsState } from '../../hooks/settings/use-settings-screen'; // Centralized color palette shared with Tailwind const colors = require('../../theme/colors.json'); @@ -96,13 +97,6 @@ interface SettingsSection { rows: SettingsRow[]; } -interface PrefsState { - darkMode: boolean; - notifications: boolean; - biometric: boolean; - autoPay: boolean; -} - const SECTIONS: SettingsSection[] = [ { title: 'ACCOUNT', @@ -138,16 +132,7 @@ const SECTIONS: SettingsSection[] = [ const SettingsScreen: React.FC = ({ onBack }) => { const insets = useSafeAreaInsets(); - const [prefs, setPrefs] = useState({ - darkMode: false, - notifications: true, - biometric: false, - autoPay: true, - }); - - const togglePref = (key: keyof PrefsState) => { - setPrefs((prev) => ({ ...prev, [key]: !prev[key] })); - }; + const { prefs, togglePref } = useSettingsScreen(); return ( diff --git a/hooks/settings/use-settings-screen.test.ts b/hooks/settings/use-settings-screen.test.ts new file mode 100644 index 0000000..118b5e1 --- /dev/null +++ b/hooks/settings/use-settings-screen.test.ts @@ -0,0 +1,95 @@ +import { renderHook, act } from '@testing-library/react-hooks'; +import { useSettingsScreen } from './use-settings-screen'; + +declare var describe: any; +declare var it: any; +declare var expect: any; + +describe('useSettingsScreen', () => { + it('initializes with default preferences', () => { + const { result } = renderHook(() => useSettingsScreen()); + + expect(result.current.prefs).toEqual({ + darkMode: false, + notifications: true, + biometric: false, + autoPay: true, + }); + }); + + it('toggles darkMode preference', () => { + const { result } = renderHook(() => useSettingsScreen()); + + act(() => { + result.current.togglePref('darkMode'); + }); + + expect(result.current.prefs.darkMode).toBe(true); + + act(() => { + result.current.togglePref('darkMode'); + }); + + expect(result.current.prefs.darkMode).toBe(false); + }); + + it('toggles notifications preference', () => { + const { result } = renderHook(() => useSettingsScreen()); + + expect(result.current.prefs.notifications).toBe(true); + + act(() => { + result.current.togglePref('notifications'); + }); + + expect(result.current.prefs.notifications).toBe(false); + }); + + it('toggles biometric preference', () => { + const { result } = renderHook(() => useSettingsScreen()); + + expect(result.current.prefs.biometric).toBe(false); + + act(() => { + result.current.togglePref('biometric'); + }); + + expect(result.current.prefs.biometric).toBe(true); + }); + + it('toggles autoPay preference', () => { + const { result } = renderHook(() => useSettingsScreen()); + + expect(result.current.prefs.autoPay).toBe(true); + + act(() => { + result.current.togglePref('autoPay'); + }); + + expect(result.current.prefs.autoPay).toBe(false); + }); + + it('toggles multiple preferences independently', () => { + const { result } = renderHook(() => useSettingsScreen()); + + act(() => { + result.current.togglePref('darkMode'); + result.current.togglePref('biometric'); + }); + + expect(result.current.prefs.darkMode).toBe(true); + expect(result.current.prefs.biometric).toBe(true); + expect(result.current.prefs.notifications).toBe(true); // unchanged + expect(result.current.prefs.autoPay).toBe(true); // unchanged + }); + + it('creates toggle animation with correct initial value', () => { + const { result } = renderHook(() => useSettingsScreen()); + + const animationOn = result.current.createToggleAnimation(true); + const animationOff = result.current.createToggleAnimation(false); + + expect(animationOn.anim).toBeDefined(); + expect(animationOff.anim).toBeDefined(); + }); +}); diff --git a/hooks/settings/use-settings-screen.ts b/hooks/settings/use-settings-screen.ts new file mode 100644 index 0000000..cc7b208 --- /dev/null +++ b/hooks/settings/use-settings-screen.ts @@ -0,0 +1,69 @@ +import { useState, useCallback, useRef } from 'react'; +import { Animated } from 'react-native'; + +/** + * Preferences state for settings toggles + */ +export interface PrefsState { + darkMode: boolean; + notifications: boolean; + biometric: boolean; + autoPay: boolean; +} + +/** + * Animation state for a single toggle + */ +export interface ToggleAnimationState { + anim: Animated.Value; +} + +/** + * Return type for the useSettingsScreen hook + */ +export interface UseSettingsScreenReturn { + // Preferences state + prefs: PrefsState; + + // Handlers + togglePref: (key: keyof PrefsState) => void; + + // Animation factory for CustomToggle components + createToggleAnimation: (initialValue: boolean) => ToggleAnimationState; +} + +/** + * Initial preferences state + */ +const initialPrefsState: PrefsState = { + darkMode: false, + notifications: true, + biometric: false, + autoPay: true, +}; + +/** + * Custom hook for Settings Screen logic + * Handles preferences state, toggle handlers, and animation setup + */ +export const useSettingsScreen = (): UseSettingsScreenReturn => { + const [prefs, setPrefs] = useState(initialPrefsState); + + // Toggle preference handler + const togglePref = useCallback((key: keyof PrefsState) => { + setPrefs((prev: PrefsState) => ({ ...prev, [key]: !prev[key] })); + }, []); + + // Factory function to create animation state for toggles + const createToggleAnimation = useCallback((initialValue: boolean): ToggleAnimationState => { + return { + anim: new Animated.Value(initialValue ? 1 : 0), + }; + }, []); + + return { + prefs, + togglePref, + createToggleAnimation, + }; +};