diff --git a/App.tsx b/App.tsx
index ad146f1..c9c50a3 100644
--- a/App.tsx
+++ b/App.tsx
@@ -11,6 +11,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import {ThemeProvider} from './src/context/ThemeContext';
import {DialogProvider} from './src/context/DialogContext';
+import {AppLockProvider} from './src/context/AppLockContext';
import {SheetProvider} from 'react-native-actions-sheet';
import ErrorBoundary from './src/components/atoms/ErrorBoundary';
import PrimaryText from './src/components/atoms/PrimaryText';
@@ -71,11 +72,13 @@ const App = () => {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/ios/zero/Info.plist b/ios/zero/Info.plist
index 150356b..257139b 100644
--- a/ios/zero/Info.plist
+++ b/ios/zero/Info.plist
@@ -35,6 +35,8 @@
NSLocationWhenInUseUsageDescription
+ NSFaceIDUsageDescription
+ Zero uses Face ID to protect your financial data
RCTNewArchEnabled
UIAppFonts
diff --git a/package.json b/package.json
index ac6dd14..d3631d8 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"react-i18next": "^17.0.8",
"react-native": "0.84.1",
"react-native-actions-sheet": "^10.1.2",
+ "react-native-biometrics": "^3.0.1",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.30.0",
"react-native-get-random-values": "^2.0.0",
diff --git a/src/components/atoms/LockScreen.tsx b/src/components/atoms/LockScreen.tsx
new file mode 100644
index 0000000..da19667
--- /dev/null
+++ b/src/components/atoms/LockScreen.tsx
@@ -0,0 +1,58 @@
+import React, {useEffect, useRef} from 'react';
+import {Animated, TouchableOpacity, View} from 'react-native';
+import {useTranslation} from 'react-i18next';
+import PrimaryView from './PrimaryView';
+import PrimaryText from './PrimaryText';
+import Icon from './Icons';
+import {useThemeColors} from '../../context/ThemeContext';
+import {gs} from '../../styles/globalStyles';
+
+interface LockScreenProps {
+ onAuthenticate: () => void;
+}
+
+const LockScreen: React.FC = ({onAuthenticate}) => {
+ const colors = useThemeColors();
+ const {t} = useTranslation();
+ const fadeAnim = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ Animated.timing(fadeAnim, {
+ toValue: 1,
+ duration: 300,
+ useNativeDriver: true,
+ }).start();
+ }, [fadeAnim]);
+
+ return (
+
+
+
+
+
+
+ zero
+
+
+ {t('settings.appLockPrompt')}
+
+
+
+
+ Tap to unlock
+
+
+
+
+ );
+};
+
+export default LockScreen;
diff --git a/src/context/AppLockContext.tsx b/src/context/AppLockContext.tsx
new file mode 100644
index 0000000..9e52deb
--- /dev/null
+++ b/src/context/AppLockContext.tsx
@@ -0,0 +1,161 @@
+import React, {createContext, useContext, useState, useEffect, useCallback, type ReactNode} from 'react';
+import {AppState, Modal} from 'react-native';
+import ReactNativeBiometrics from 'react-native-biometrics';
+import StorageService from '../utils/asyncStorageService';
+import LockScreen from '../components/atoms/LockScreen';
+import {useTranslation} from 'react-i18next';
+import {useSelector} from 'react-redux';
+import {selectIsOnboarded} from '../redux/slice/isOnboardedSlice';
+
+interface AppLockContextType {
+ isAppLockEnabled: boolean;
+ isBiometricAvailable: boolean;
+ toggleAppLock: () => Promise;
+}
+
+const AppLockContext = createContext(undefined);
+
+interface AppLockProviderProps {
+ children: ReactNode;
+}
+
+const rnBiometrics = new ReactNativeBiometrics();
+const APP_LOCK_ENABLED_KEY = 'appLockEnabled';
+
+export const AppLockProvider: React.FC = ({children}) => {
+ const {t} = useTranslation();
+ const isOnboarded = useSelector(selectIsOnboarded);
+
+ const [isAppLockEnabled, setIsAppLockEnabled] = useState(false);
+ const [isBiometricAvailable, setIsBiometricAvailable] = useState(false);
+ const [isLocked, setIsLocked] = useState(false);
+ const [appState, setAppState] = useState(AppState.currentState);
+
+ // Initialize
+ useEffect(() => {
+ const checkAvailability = async () => {
+ try {
+ const {available} = await rnBiometrics.isSensorAvailable();
+ setIsBiometricAvailable(available);
+
+ // Read preference
+ const enabled = StorageService.getBoolean(APP_LOCK_ENABLED_KEY);
+
+ if (enabled) {
+ if (available) {
+ setIsAppLockEnabled(true);
+ setIsLocked(true); // Lock on startup if enabled
+ } else {
+ // Graceful fallback: sensor no longer available, disable lock
+ StorageService.setBoolean(APP_LOCK_ENABLED_KEY, false);
+ setIsAppLockEnabled(false);
+ setIsLocked(false);
+ }
+ }
+ } catch (error) {
+ if (__DEV__) console.error('Biometrics check error:', error);
+ }
+ };
+
+ checkAvailability();
+ }, []);
+
+ const authenticate = useCallback(async () => {
+ if (!isBiometricAvailable) return false;
+
+ try {
+ const {success} = await rnBiometrics.simplePrompt({
+ promptMessage: t('settings.appLockPrompt') || 'Unlock Zero',
+ });
+
+ if (success) {
+ setIsLocked(false);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ if (__DEV__) console.error('Authentication error:', error);
+ return false;
+ }
+ }, [isBiometricAvailable, t]);
+
+ // Handle App State changes (background -> foreground)
+ useEffect(() => {
+ const subscription = AppState.addEventListener('change', nextAppState => {
+ if (appState.match(/inactive|background/) && nextAppState === 'active') {
+ if (isAppLockEnabled && isOnboarded) {
+ setIsLocked(true);
+ }
+ }
+ setAppState(nextAppState);
+ });
+
+ return () => {
+ subscription.remove();
+ };
+ }, [appState, isAppLockEnabled, isOnboarded]);
+
+ // Auto-authenticate when locked screen appears
+ useEffect(() => {
+ let timer: ReturnType;
+ if (isLocked && isOnboarded) {
+ // Small timeout to ensure UI renders before native prompt
+ timer = setTimeout(() => {
+ void authenticate();
+ }, 500);
+ }
+ return () => {
+ if (timer) clearTimeout(timer);
+ };
+ }, [isLocked, isOnboarded, authenticate]);
+
+ const toggleAppLock = useCallback(async () => {
+ if (!isBiometricAvailable) return false;
+
+ if (isAppLockEnabled) {
+ // Disabling requires authentication
+ const success = await authenticate();
+ if (success) {
+ StorageService.setBoolean(APP_LOCK_ENABLED_KEY, false);
+ setIsAppLockEnabled(false);
+ }
+ return !success; // return true if still enabled
+ } else {
+ // Enabling requires authentication to prove it works
+ const success = await authenticate();
+ if (success) {
+ StorageService.setBoolean(APP_LOCK_ENABLED_KEY, true);
+ setIsAppLockEnabled(true);
+ }
+ return success; // return true if successfully enabled
+ }
+ }, [isAppLockEnabled, isBiometricAvailable, authenticate]);
+
+ const value = {
+ isAppLockEnabled,
+ isBiometricAvailable,
+ toggleAppLock,
+ };
+
+ // Only show lock screen if user is onboarded
+ const showLockScreen = isLocked && isOnboarded;
+
+ return (
+
+ {children}
+
+
+
+
+ );
+};
+
+export const useAppLock = (): AppLockContextType => {
+ const context = useContext(AppLockContext);
+ if (context === undefined) {
+ throw new Error('useAppLock must be used within an AppLockProvider');
+ }
+ return context;
+};
+
+export default AppLockContext;
diff --git a/src/context/index.ts b/src/context/index.ts
index fe4213c..3f0d308 100644
--- a/src/context/index.ts
+++ b/src/context/index.ts
@@ -8,3 +8,5 @@ export {
} from './ThemeContext';
export {DialogProvider, useDialog} from './DialogContext';
+
+export {AppLockProvider, useAppLock} from './AppLockContext';
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 62e2792..a40f45c 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -197,7 +197,13 @@
"exportSuccess": "Your data is successfully exported in Downloads folder",
"exportError": "There is an error in exporting your data",
"storagePermission": "You need to manually give permission for the storage to download your data",
- "language": "Language"
+ "language": "Language",
+ "appLock": "App Lock",
+ "appLockSubtitle": "Unlock with biometrics",
+ "appLockPrompt": "Unlock Zero",
+ "appLockNotAvailable": "Biometric authentication is not available on this device",
+ "appLockEnabled": "App lock enabled",
+ "appLockDisabled": "App lock disabled"
},
"sheets": {
"selectTheme": "Select Theme",
diff --git a/src/screens/SettingsScreen/index.tsx b/src/screens/SettingsScreen/index.tsx
index f31601d..65450cb 100644
--- a/src/screens/SettingsScreen/index.tsx
+++ b/src/screens/SettingsScreen/index.tsx
@@ -71,6 +71,9 @@ const SettingsScreen = () => {
allData,
handleExportResult,
requestStorageViaDialog,
+ isAppLockEnabled,
+ isBiometricAvailable,
+ toggleAppLock,
} = useSettings();
const handleOpenCurrencySheet = useCallback(() => {
@@ -209,6 +212,27 @@ const SettingsScreen = () => {
});
}}
/>
+ {isBiometricAvailable ? (
+ <>
+
+ toggleAppLock()}
+ valueNode={
+
+ }
+ />
+ >
+ ) : null}
{
const allData = useSelector(selectAllData);
const {colors, themeMode, setThemeMode} = useTheme();
+ const {isAppLockEnabled, isBiometricAvailable, toggleAppLock} = useAppLock();
const {showDialog, showAlert} = useDialog();
const appVersion = getAppVersion();
@@ -201,6 +203,9 @@ const useSettings = () => {
allData,
handleExportResult,
requestStorageViaDialog,
+ isAppLockEnabled,
+ isBiometricAvailable,
+ toggleAppLock,
};
};