From 262ba0bb83f88ca4cffe68f37939dba47042eb57 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 13 Sep 2024 12:51:59 +0530 Subject: [PATCH 1/7] feat: add biometrics --- app.json | 6 +++ app/_layout.tsx | 32 ++++++++++-- app/settings/security.js | 5 ++ app/unlock.js | 5 ++ components/Icons.tsx | 3 ++ components/ui/switch.tsx | 86 ++++++++++++++++++++++++++++++++ context/UserInactivity.tsx | 42 ++++++++++++++++ lib/constants.ts | 13 +++++ lib/state/appStore.ts | 35 +++++++++++++ package.json | 2 + pages/Unlock.tsx | 56 +++++++++++++++++++++ pages/settings/Security.tsx | 31 ++++++++++++ pages/settings/Settings.tsx | 14 +++++- yarn.lock | 97 +++++++++++++++++++++++++++++++++++++ 14 files changed, 422 insertions(+), 5 deletions(-) create mode 100644 app/settings/security.js create mode 100644 app/unlock.js create mode 100644 components/ui/switch.tsx create mode 100644 context/UserInactivity.tsx create mode 100644 pages/Unlock.tsx create mode 100644 pages/settings/Security.tsx diff --git a/app.json b/app.json index 6dbed32..d2e26ba 100644 --- a/app.json +++ b/app.json @@ -14,6 +14,12 @@ }, "assetBundlePatterns": ["**/*"], "plugins": [ + [ + "expo-local-authentication", + { + "faceIDPermission": "Allow Alby Go to use Face ID." + } + ], [ "expo-camera", { diff --git a/app/_layout.tsx b/app/_layout.tsx index 4a6de40..06ac2c7 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,6 +11,7 @@ import { import { StatusBar } from "expo-status-bar"; import * as React from "react"; import { SafeAreaView } from "react-native"; +import * as LocalAuthentication from "expo-local-authentication"; import { NAV_THEME } from "~/lib/constants"; import { useColorScheme } from "~/lib/useColorScheme"; import PolyfillCrypto from "react-native-webview-crypto"; @@ -21,7 +22,9 @@ import { toastConfig } from "~/components/ToastConfig"; import * as Font from "expo-font"; import { useInfo } from "~/hooks/useInfo"; import { secureStorage } from "~/lib/secureStorage"; -import { hasOnboardedKey } from "~/lib/state/appStore"; +import { hasOnboardedKey, useAppStore } from "~/lib/state/appStore"; +import { usePathname } from "expo-router"; +import { UserInactivityProvider } from "~/context/UserInactivity"; const LIGHT_THEME: Theme = { dark: false, @@ -48,6 +51,8 @@ export default function RootLayout() { const { isDarkColorScheme } = useColorScheme(); const [fontsLoaded, setFontsLoaded] = React.useState(false); const [checkedOnboarding, setCheckedOnboarding] = React.useState(false); + const isUnlocked = useAppStore((store) => store.unlocked); + const pathname = usePathname(); useConnectionChecker(); const rootNavigationState = useRootNavigationState(); @@ -63,7 +68,6 @@ export default function RootLayout() { }; async function loadFonts() { - await Font.loadAsync({ OpenRunde: require("./../assets/fonts/OpenRunde-Regular.otf"), "OpenRunde-Medium": require("./../assets/fonts/OpenRunde-Medium.otf"), @@ -74,12 +78,23 @@ export default function RootLayout() { setFontsLoaded(true); } + async function checkBiometricStatus() { + const compatible = await LocalAuthentication.hasHardwareAsync(); + const enrolled = await LocalAuthentication.isEnrolledAsync(); + if (compatible && enrolled) { + useAppStore.getState().setBiometricSupported(true); + } else { + useAppStore.getState().setBiometricSupported(false); + } + } + React.useEffect(() => { const init = async () => { try { await Promise.all([ checkOnboardingStatus(), loadFonts(), + checkBiometricStatus(), ]); } finally { @@ -88,9 +103,16 @@ export default function RootLayout() { }; init(); - }, [hasNavigationState]); + React.useEffect(() => { + if (hasNavigationState && !isUnlocked) { + if (pathname !== "/unlock") { + router.push("/unlock"); + } + } + }, [isUnlocked, hasNavigationState]); + if (!fontsLoaded || !checkedOnboarding) { return null; } @@ -101,7 +123,9 @@ export default function RootLayout() { - + + + diff --git a/app/settings/security.js b/app/settings/security.js new file mode 100644 index 0000000..1dca538 --- /dev/null +++ b/app/settings/security.js @@ -0,0 +1,5 @@ +import { Security } from "../../pages/settings/Security"; + +export default function Page() { + return ; +} diff --git a/app/unlock.js b/app/unlock.js new file mode 100644 index 0000000..b17a77d --- /dev/null +++ b/app/unlock.js @@ -0,0 +1,5 @@ +import { Unlock } from "../pages/Unlock"; + +export default function Page() { + return ; +} diff --git a/components/Icons.tsx b/components/Icons.tsx index 2fe4bf0..e499204 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -34,6 +34,7 @@ import { CameraOff, Palette, Egg, + Fingerprint, } from "lucide-react-native"; import { cssInterop } from "nativewind"; @@ -83,6 +84,7 @@ interopIcon(Power); interopIcon(CameraOff); interopIcon(Palette); interopIcon(Egg); +interopIcon(Fingerprint); export { AlertCircle, @@ -119,4 +121,5 @@ export { Power, Palette, Egg, + Fingerprint, }; diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx new file mode 100644 index 0000000..41ec9d1 --- /dev/null +++ b/components/ui/switch.tsx @@ -0,0 +1,86 @@ +import * as SwitchPrimitives from '@rn-primitives/switch'; +import * as React from 'react'; +import { Platform } from 'react-native'; +import Animated, { + interpolateColor, + useAnimatedStyle, + useDerivedValue, + withTiming, +} from 'react-native-reanimated'; +import { SWITCH_THEME } from '~/lib/constants'; +import { useColorScheme } from '~/lib/useColorScheme'; +import { cn } from '~/lib/utils'; + +const SwitchWeb = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); + +SwitchWeb.displayName = 'SwitchWeb'; + +const SwitchNative = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { colorScheme } = useColorScheme(); + const translateX = useDerivedValue(() => (props.checked ? 18 : 0)); + const animatedRootStyle = useAnimatedStyle(() => { + return { + backgroundColor: interpolateColor( + Number(props.checked), + [0, 1], + [SWITCH_THEME[colorScheme].input, SWITCH_THEME[colorScheme].primary] + ), + }; + }); + const animatedThumbStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: withTiming(translateX.value, { duration: 200 }) }], + })); + return ( + + + + + + + + ); +}); +SwitchNative.displayName = 'SwitchNative'; + +const Switch = Platform.select({ + web: SwitchWeb, + default: SwitchNative, +}); + +export { Switch }; diff --git a/context/UserInactivity.tsx b/context/UserInactivity.tsx new file mode 100644 index 0000000..26bed57 --- /dev/null +++ b/context/UserInactivity.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native'; +import { secureStorage } from "~/lib/secureStorage"; +import { INACTIVITY_THRESHOLD } from "~/lib/constants"; +import { useAppStore } from "~/lib/state/appStore"; + +export const UserInactivityProvider = ({ children }: any) => { + const [appState, setAppState] = React.useState(AppState.currentState); + const isSecurityEnabled = useAppStore((store) => store.isSecurityEnabled); + + const handleAppStateChange = async (nextState: AppStateStatus) => { + if (appState === "active" && nextState.match(/inactive|background/)) { + const now = Date.now(); + secureStorage.setItem("lastActiveTime", now.toString()); + } else if (appState.match(/inactive|background/) && nextState === "active") { + const lastActiveTime = secureStorage.getItem("lastActiveTime"); + if (lastActiveTime) { + const timeElapsed = Date.now() - parseInt(lastActiveTime, 10); + if (timeElapsed >= INACTIVITY_THRESHOLD) { + useAppStore.getState().setUnlocked(false) + } + } + await secureStorage.removeItem("lastActiveTime"); + } + setAppState(nextState); + }; + + React.useEffect(() => { + let subscription: NativeEventSubscription + if (isSecurityEnabled) { + subscription = AppState.addEventListener("change", handleAppStateChange); + } + + return () => { + if (subscription) { + subscription.remove(); + } + }; + }, [appState, isSecurityEnabled]); + + return children; +} \ No newline at end of file diff --git a/lib/constants.ts b/lib/constants.ts index 46c2eae..31d452d 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -17,6 +17,19 @@ export const NAV_THEME = { }, }; +export const SWITCH_THEME = { + light: { + primary: 'rgb(255, 224, 112)', + input: 'rgb(228, 228, 231)', + }, + dark: { + primary: 'rgb(255, 224, 112)', + input: 'rgb(39, 39, 42)', + }, +}; + +export const INACTIVITY_THRESHOLD = 5 * 1000 // 5 * 60 * 1000; + export const CURSOR_COLOR = "hsl(47 100% 72%)"; export const TRANSACTIONS_PAGE_SIZE = 20; diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index f5a827d..0979bcc 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -4,11 +4,15 @@ import { nwc } from "@getalby/sdk"; import { secureStorage } from "lib/secureStorage"; interface AppState { + readonly unlocked: boolean; readonly nwcClient: NWCClient | undefined; readonly fiatCurrency: string; readonly selectedWalletId: number; readonly wallets: Wallet[]; readonly addressBookEntries: AddressBookEntry[]; + readonly isSecurityEnabled: boolean; + readonly isBiometricSupported: boolean; + setUnlocked: (unlocked: boolean) => void; setNWCClient: (nwcClient: NWCClient | undefined) => void; setNostrWalletConnectUrl(nostrWalletConnectUrl: string): void; removeNostrWalletConnectUrl(): void; @@ -16,6 +20,8 @@ interface AppState { removeCurrentWallet(): void; setFiatCurrency(fiatCurrency: string): void; setSelectedWalletId(walletId: number): void; + setSecurityEnabled(securityEnabled: boolean): void; + setBiometricSupported(isSupported: boolean): void; addWallet(wallet: Wallet): void; addAddressBookEntry(entry: AddressBookEntry): void; reset(): void; @@ -26,7 +32,10 @@ const walletKeyPrefix = "wallet"; const addressBookEntryKeyPrefix = "addressBookEntry"; const selectedWalletIdKey = "selectedWalletId"; const fiatCurrencyKey = "fiatCurrency"; +export const isSecurityEnabledKey = "isSecurityEnabled"; +export const isBiometricSupportedKey = "isBiometricSupported"; export const hasOnboardedKey = "hasOnboarded"; +export const lastActiveTimeKey = "lastActiveTime"; type Wallet = { name?: string; @@ -124,15 +133,25 @@ export const useAppStore = create()((set, get) => { const initialSelectedWalletId = +( secureStorage.getItem(selectedWalletIdKey) || "0" ); + + const isBiometricSupported = secureStorage.getItem(isBiometricSupportedKey) === "true"; + const iSecurityEnabled = isBiometricSupported && secureStorage.getItem(isSecurityEnabledKey) === "true"; + const initialWallets = loadWallets(); return { + unlocked: !iSecurityEnabled, addressBookEntries: loadAddressBookEntries(), wallets: initialWallets, nwcClient: getNWCClient(initialSelectedWalletId), fiatCurrency: secureStorage.getItem(fiatCurrencyKey) || "", + isSecurityEnabled: iSecurityEnabled, + isBiometricSupported: isBiometricSupported, selectedWalletId: initialSelectedWalletId, updateCurrentWallet, removeCurrentWallet, + setUnlocked: (unlocked) => { + set({ unlocked }); + }, setNWCClient: (nwcClient) => set({ nwcClient }), removeNostrWalletConnectUrl: () => { updateCurrentWallet({ @@ -146,6 +165,18 @@ export const useAppStore = create()((set, get) => { nostrWalletConnectUrl, }); }, + setSecurityEnabled: (isEnabled) => { + secureStorage.setItem(isSecurityEnabledKey, isEnabled ? "true" : "false"); + set({ isSecurityEnabled: isEnabled }); + }, + setBiometricSupported: (isSupported) => { + secureStorage.setItem(isBiometricSupportedKey, isSupported ? "true" : "false"); + set({ isBiometricSupported: isSupported }); + if (!isSupported) { + secureStorage.setItem(isSecurityEnabledKey, "false"); + set({ isSecurityEnabled: false, unlocked: true }); + } + }, setFiatCurrency: (fiatCurrency) => { secureStorage.setItem(fiatCurrencyKey, fiatCurrency); set({ fiatCurrency }); @@ -194,6 +225,9 @@ export const useAppStore = create()((set, get) => { // clear selected wallet ID secureStorage.removeItem(selectedWalletIdKey); + // clear security enabled status + secureStorage.removeItem(isSecurityEnabledKey); + // clear onboarding status secureStorage.removeItem(hasOnboardedKey); @@ -203,6 +237,7 @@ export const useAppStore = create()((set, get) => { selectedWalletId: undefined, wallets: [], addressBookEntries: [], + isSecurityEnabled: false, }); }, }; diff --git a/package.json b/package.json index ea8b1d3..c158f6e 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@getalby/lightning-tools": "^5.0.3", "@getalby/sdk": "^3.7.0", "@react-native-async-storage/async-storage": "1.23.1", + "@rn-primitives/switch": "^1.0.3", "bech32": "^2.0.0", "buffer": "^6.0.3", "class-variance-authority": "^0.7.0", @@ -31,6 +32,7 @@ "expo-font": "^12.0.9", "expo-linear-gradient": "~13.0.2", "expo-linking": "~6.3.1", + "expo-local-authentication": "~14.0.1", "expo-router": "^3.5.23", "expo-secure-store": "^13.0.2", "expo-status-bar": "~1.12.1", diff --git a/pages/Unlock.tsx b/pages/Unlock.tsx new file mode 100644 index 0000000..d2110b0 --- /dev/null +++ b/pages/Unlock.tsx @@ -0,0 +1,56 @@ +import { router, Stack } from "expo-router"; +import React from "react"; +import { View, Image } from "react-native"; +import * as LocalAuthentication from "expo-local-authentication"; + +import { Button } from "~/components/ui/button"; +import { Text } from "~/components/ui/text"; +import { useAppStore } from "~/lib/state/appStore"; + +export function Unlock() { + const [isUnlocking, setIsUnlocking] = React.useState(false); + + const handleUnlock = async () => { + try { + setIsUnlocking(true); + const biometricAuth = await LocalAuthentication.authenticateAsync({ + promptMessage: "Unlock Alby Go", + }); + if (biometricAuth.success) { + useAppStore.getState().setUnlocked(true); + if (router.canGoBack()) { + router.back(); + } else { + router.replace("/"); + } + } + } finally { + setIsUnlocking(false); + } + }; + + React.useEffect(() => { + handleUnlock(); + }, []); + + return ( + + + + + Unlock to continue + + + + ); +} diff --git a/pages/settings/Security.tsx b/pages/settings/Security.tsx new file mode 100644 index 0000000..2d76e12 --- /dev/null +++ b/pages/settings/Security.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { View, Text } from "react-native"; +import { Label } from "~/components/ui/label"; +import { Switch } from "~/components/ui/switch"; +import Screen from "~/components/Screen"; +import { useAppStore } from "~/lib/state/appStore"; + +export function Security() { + const isEnabled = useAppStore((store) => store.isSecurityEnabled); + return ( + + + + + + { + useAppStore.getState().setSecurityEnabled(!isEnabled); + }} + nativeID='security' /> + + + + ); +} diff --git a/pages/settings/Settings.tsx b/pages/settings/Settings.tsx index 58f51e3..ba7612a 100644 --- a/pages/settings/Settings.tsx +++ b/pages/settings/Settings.tsx @@ -1,6 +1,6 @@ import { Link, router } from "expo-router"; import { Alert, TouchableOpacity, View } from "react-native"; -import { Bitcoin, Egg, Palette, Power, Wallet2 } from "~/components/Icons"; +import { Bitcoin, Egg, Fingerprint, Palette, Power, Wallet2 } from "~/components/Icons"; import { DEFAULT_WALLET_NAME } from "~/lib/constants"; import { useAppStore } from "~/lib/state/appStore"; @@ -13,6 +13,7 @@ import Screen from "~/components/Screen"; export function Settings() { const wallet = useAppStore((store) => store.wallets[store.selectedWalletId]); + const isBiometricSupported = useAppStore((store) => store.isBiometricSupported); const [developerCounter, setDeveloperCounter] = React.useState(0); const [developerMode, setDeveloperMode] = React.useState(false); const { colorScheme, toggleColorScheme } = useColorScheme(); @@ -41,6 +42,17 @@ export function Settings() { + {isBiometricSupported && ( + + + + + Security + + + + )} + Date: Tue, 17 Sep 2024 15:16:21 +0530 Subject: [PATCH 2/7] chore: add alert in security page --- app/_layout.tsx | 4 ++-- components/Icons.tsx | 3 +++ pages/settings/Security.tsx | 17 ++++++++++++++++- pages/settings/Settings.tsx | 18 ++++++++---------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/_layout.tsx b/app/_layout.tsx index 695d468..4d5301a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -81,8 +81,8 @@ export default function RootLayout() { async function checkBiometricStatus() { const compatible = await LocalAuthentication.hasHardwareAsync(); - const enrolled = await LocalAuthentication.isEnrolledAsync(); - if (compatible && enrolled) { + const securityLevel = await LocalAuthentication.getEnrolledLevelAsync(); + if (compatible && securityLevel > 0) { useAppStore.getState().setBiometricSupported(true); } else { useAppStore.getState().setBiometricSupported(false); diff --git a/components/Icons.tsx b/components/Icons.tsx index 8c0cf55..51d14a2 100644 --- a/components/Icons.tsx +++ b/components/Icons.tsx @@ -37,6 +37,7 @@ import { Fingerprint, HelpCircle, CircleCheck, + TriangleAlert, } from "lucide-react-native"; import { cssInterop } from "nativewind"; @@ -89,6 +90,7 @@ interopIcon(Egg); interopIcon(Fingerprint); interopIcon(HelpCircle); interopIcon(CircleCheck); +interopIcon(TriangleAlert); export { AlertCircle, @@ -128,4 +130,5 @@ export { Fingerprint, HelpCircle, CircleCheck, + TriangleAlert, }; diff --git a/pages/settings/Security.tsx b/pages/settings/Security.tsx index 2d76e12..b15372a 100644 --- a/pages/settings/Security.tsx +++ b/pages/settings/Security.tsx @@ -4,22 +4,37 @@ import { Label } from "~/components/ui/label"; import { Switch } from "~/components/ui/switch"; import Screen from "~/components/Screen"; import { useAppStore } from "~/lib/state/appStore"; +import { TriangleAlert } from "~/components/Icons"; +import { cn } from "~/lib/utils"; export function Security() { const isEnabled = useAppStore((store) => store.isSecurityEnabled); + const isSupported = useAppStore((store) => store.isBiometricSupported); return ( + {!isSupported && ( + + + + Can't add security + + Add phone lock in device settings to secure access + + + + )} { useAppStore.getState().setSecurityEnabled(!isEnabled); }} diff --git a/pages/settings/Settings.tsx b/pages/settings/Settings.tsx index a3bfb0d..6825aaf 100644 --- a/pages/settings/Settings.tsx +++ b/pages/settings/Settings.tsx @@ -44,16 +44,14 @@ export function Settings() { - {isBiometricSupported && ( - - - - - Security - - - - )} + + + + + Security + + + Date: Tue, 17 Sep 2024 15:19:56 +0530 Subject: [PATCH 3/7] chore: remove switch colors from constants --- components/ui/switch.tsx | 14 ++++++++++++-- lib/constants.ts | 11 ----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx index 41ec9d1..951716d 100644 --- a/components/ui/switch.tsx +++ b/components/ui/switch.tsx @@ -7,7 +7,6 @@ import Animated, { useDerivedValue, withTiming, } from 'react-native-reanimated'; -import { SWITCH_THEME } from '~/lib/constants'; import { useColorScheme } from '~/lib/useColorScheme'; import { cn } from '~/lib/utils'; @@ -36,6 +35,17 @@ const SwitchWeb = React.forwardRef< SwitchWeb.displayName = 'SwitchWeb'; +const RGB_COLORS = { + light: { + primary: 'rgb(255, 224, 112)', + input: 'rgb(228, 228, 231)', + }, + dark: { + primary: 'rgb(255, 224, 112)', + input: 'rgb(228, 228, 231)', + }, +} as const; + const SwitchNative = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -47,7 +57,7 @@ const SwitchNative = React.forwardRef< backgroundColor: interpolateColor( Number(props.checked), [0, 1], - [SWITCH_THEME[colorScheme].input, SWITCH_THEME[colorScheme].primary] + [RGB_COLORS[colorScheme].input, RGB_COLORS[colorScheme].primary] ), }; }); diff --git a/lib/constants.ts b/lib/constants.ts index 31d452d..9da19c3 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -17,17 +17,6 @@ export const NAV_THEME = { }, }; -export const SWITCH_THEME = { - light: { - primary: 'rgb(255, 224, 112)', - input: 'rgb(228, 228, 231)', - }, - dark: { - primary: 'rgb(255, 224, 112)', - input: 'rgb(39, 39, 42)', - }, -}; - export const INACTIVITY_THRESHOLD = 5 * 1000 // 5 * 60 * 1000; export const CURSOR_COLOR = "hsl(47 100% 72%)"; From 732adeea06f41cb4361b6b3c7824e71f5ff8072c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Aaron?= Date: Tue, 17 Sep 2024 14:04:05 +0200 Subject: [PATCH 4/7] fix: copy & component usage --- pages/settings/Security.tsx | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pages/settings/Security.tsx b/pages/settings/Security.tsx index b15372a..0b7c7d2 100644 --- a/pages/settings/Security.tsx +++ b/pages/settings/Security.tsx @@ -6,29 +6,33 @@ import Screen from "~/components/Screen"; import { useAppStore } from "~/lib/state/appStore"; import { TriangleAlert } from "~/components/Icons"; import { cn } from "~/lib/utils"; +import { Card, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; export function Security() { const isEnabled = useAppStore((store) => store.isSecurityEnabled); - const isSupported = useAppStore((store) => store.isBiometricSupported); + const isSupported = false; return ( {!isSupported && ( - - - - Can't add security - - Add phone lock in device settings to secure access - - - + + + + + {" "}Setup Device Security + + + To protect your wallet, please set up a phone lock in your device settings first. + + + )} - + @@ -38,7 +42,7 @@ export function Security() { onCheckedChange={() => { useAppStore.getState().setSecurityEnabled(!isEnabled); }} - nativeID='security' /> + nativeID="security" /> From d346cba795cd705109fe5a7d75c4724a39da9d8b Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 17 Sep 2024 19:15:22 +0530 Subject: [PATCH 5/7] chore: fix inactivity threshold and use key constant --- context/UserInactivity.tsx | 8 ++++---- lib/constants.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/context/UserInactivity.tsx b/context/UserInactivity.tsx index 26bed57..2133fd7 100644 --- a/context/UserInactivity.tsx +++ b/context/UserInactivity.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native'; import { secureStorage } from "~/lib/secureStorage"; import { INACTIVITY_THRESHOLD } from "~/lib/constants"; -import { useAppStore } from "~/lib/state/appStore"; +import { lastActiveTimeKey, useAppStore } from "~/lib/state/appStore"; export const UserInactivityProvider = ({ children }: any) => { const [appState, setAppState] = React.useState(AppState.currentState); @@ -11,16 +11,16 @@ export const UserInactivityProvider = ({ children }: any) => { const handleAppStateChange = async (nextState: AppStateStatus) => { if (appState === "active" && nextState.match(/inactive|background/)) { const now = Date.now(); - secureStorage.setItem("lastActiveTime", now.toString()); + secureStorage.setItem(lastActiveTimeKey, now.toString()); } else if (appState.match(/inactive|background/) && nextState === "active") { - const lastActiveTime = secureStorage.getItem("lastActiveTime"); + const lastActiveTime = secureStorage.getItem(lastActiveTimeKey); if (lastActiveTime) { const timeElapsed = Date.now() - parseInt(lastActiveTime, 10); if (timeElapsed >= INACTIVITY_THRESHOLD) { useAppStore.getState().setUnlocked(false) } } - await secureStorage.removeItem("lastActiveTime"); + await secureStorage.removeItem(lastActiveTimeKey); } setAppState(nextState); }; diff --git a/lib/constants.ts b/lib/constants.ts index 9da19c3..5cae282 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -17,7 +17,7 @@ export const NAV_THEME = { }, }; -export const INACTIVITY_THRESHOLD = 5 * 1000 // 5 * 60 * 1000; +export const INACTIVITY_THRESHOLD = 5 * 60 * 1000; export const CURSOR_COLOR = "hsl(47 100% 72%)"; From 6ff01f0b6d335d90d1d34def6e05791a04275f29 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Sep 2024 12:23:09 +0530 Subject: [PATCH 6/7] chore: use boolean to string method --- lib/state/appStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index 0979bcc..69af0a7 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -166,11 +166,11 @@ export const useAppStore = create()((set, get) => { }); }, setSecurityEnabled: (isEnabled) => { - secureStorage.setItem(isSecurityEnabledKey, isEnabled ? "true" : "false"); + secureStorage.setItem(isSecurityEnabledKey, isEnabled.toString()); set({ isSecurityEnabled: isEnabled }); }, setBiometricSupported: (isSupported) => { - secureStorage.setItem(isBiometricSupportedKey, isSupported ? "true" : "false"); + secureStorage.setItem(isBiometricSupportedKey, isSupported.toString()); set({ isBiometricSupported: isSupported }); if (!isSupported) { secureStorage.setItem(isSecurityEnabledKey, "false"); From cbf758f4e3089f792fbc5db5fb73f7292f632d91 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Sep 2024 21:00:57 +0530 Subject: [PATCH 7/7] chore: do not store is biometric supported --- app/_layout.tsx | 11 ++--- lib/isBiometricSupported.ts | 7 +++ lib/state/appStore.ts | 20 ++------ pages/settings/Security.tsx | 98 ++++++++++++++++++++++++------------- pages/settings/Settings.tsx | 1 - 5 files changed, 79 insertions(+), 58 deletions(-) create mode 100644 lib/isBiometricSupported.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 4d5301a..7cdc74f 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,7 +11,6 @@ import { import { StatusBar } from "expo-status-bar"; import * as React from "react"; import { SafeAreaView } from "react-native"; -import * as LocalAuthentication from "expo-local-authentication"; import { NAV_THEME } from "~/lib/constants"; import { useColorScheme } from "~/lib/useColorScheme"; import PolyfillCrypto from "react-native-webview-crypto"; @@ -26,6 +25,7 @@ import { hasOnboardedKey, useAppStore } from "~/lib/state/appStore"; import { usePathname } from "expo-router"; import { UserInactivityProvider } from "~/context/UserInactivity"; import { PortalHost } from '@rn-primitives/portal'; +import { isBiometricSupported } from "~/lib/isBiometricSupported"; const LIGHT_THEME: Theme = { dark: false, @@ -80,12 +80,9 @@ export default function RootLayout() { } async function checkBiometricStatus() { - const compatible = await LocalAuthentication.hasHardwareAsync(); - const securityLevel = await LocalAuthentication.getEnrolledLevelAsync(); - if (compatible && securityLevel > 0) { - useAppStore.getState().setBiometricSupported(true); - } else { - useAppStore.getState().setBiometricSupported(false); + const isSupported = await isBiometricSupported() + if (!isSupported) { + useAppStore.getState().setSecurityEnabled(false); } } diff --git a/lib/isBiometricSupported.ts b/lib/isBiometricSupported.ts new file mode 100644 index 0000000..bcbefd4 --- /dev/null +++ b/lib/isBiometricSupported.ts @@ -0,0 +1,7 @@ +import * as LocalAuthentication from "expo-local-authentication"; + +export async function isBiometricSupported() { + const compatible = await LocalAuthentication.hasHardwareAsync(); + const securityLevel = await LocalAuthentication.getEnrolledLevelAsync(); + return compatible && securityLevel > 0 +} \ No newline at end of file diff --git a/lib/state/appStore.ts b/lib/state/appStore.ts index 69af0a7..d33d4a7 100644 --- a/lib/state/appStore.ts +++ b/lib/state/appStore.ts @@ -11,7 +11,6 @@ interface AppState { readonly wallets: Wallet[]; readonly addressBookEntries: AddressBookEntry[]; readonly isSecurityEnabled: boolean; - readonly isBiometricSupported: boolean; setUnlocked: (unlocked: boolean) => void; setNWCClient: (nwcClient: NWCClient | undefined) => void; setNostrWalletConnectUrl(nostrWalletConnectUrl: string): void; @@ -21,7 +20,6 @@ interface AppState { setFiatCurrency(fiatCurrency: string): void; setSelectedWalletId(walletId: number): void; setSecurityEnabled(securityEnabled: boolean): void; - setBiometricSupported(isSupported: boolean): void; addWallet(wallet: Wallet): void; addAddressBookEntry(entry: AddressBookEntry): void; reset(): void; @@ -33,7 +31,6 @@ const addressBookEntryKeyPrefix = "addressBookEntry"; const selectedWalletIdKey = "selectedWalletId"; const fiatCurrencyKey = "fiatCurrency"; export const isSecurityEnabledKey = "isSecurityEnabled"; -export const isBiometricSupportedKey = "isBiometricSupported"; export const hasOnboardedKey = "hasOnboarded"; export const lastActiveTimeKey = "lastActiveTime"; @@ -134,8 +131,7 @@ export const useAppStore = create()((set, get) => { secureStorage.getItem(selectedWalletIdKey) || "0" ); - const isBiometricSupported = secureStorage.getItem(isBiometricSupportedKey) === "true"; - const iSecurityEnabled = isBiometricSupported && secureStorage.getItem(isSecurityEnabledKey) === "true"; + const iSecurityEnabled = secureStorage.getItem(isSecurityEnabledKey) === "true"; const initialWallets = loadWallets(); return { @@ -145,7 +141,6 @@ export const useAppStore = create()((set, get) => { nwcClient: getNWCClient(initialSelectedWalletId), fiatCurrency: secureStorage.getItem(fiatCurrencyKey) || "", isSecurityEnabled: iSecurityEnabled, - isBiometricSupported: isBiometricSupported, selectedWalletId: initialSelectedWalletId, updateCurrentWallet, removeCurrentWallet, @@ -167,15 +162,10 @@ export const useAppStore = create()((set, get) => { }, setSecurityEnabled: (isEnabled) => { secureStorage.setItem(isSecurityEnabledKey, isEnabled.toString()); - set({ isSecurityEnabled: isEnabled }); - }, - setBiometricSupported: (isSupported) => { - secureStorage.setItem(isBiometricSupportedKey, isSupported.toString()); - set({ isBiometricSupported: isSupported }); - if (!isSupported) { - secureStorage.setItem(isSecurityEnabledKey, "false"); - set({ isSecurityEnabled: false, unlocked: true }); - } + set({ + isSecurityEnabled: isEnabled, + ...(!isEnabled ? { unlocked: true } : {}), + }); }, setFiatCurrency: (fiatCurrency) => { secureStorage.setItem(fiatCurrencyKey, fiatCurrency); diff --git a/pages/settings/Security.tsx b/pages/settings/Security.tsx index 0b7c7d2..ea43809 100644 --- a/pages/settings/Security.tsx +++ b/pages/settings/Security.tsx @@ -1,50 +1,78 @@ import React from "react"; -import { View, Text } from "react-native"; +import { Text, View } from "react-native"; +import { TriangleAlert } from "~/components/Icons"; +import Loading from "~/components/Loading"; +import Screen from "~/components/Screen"; +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; import { Label } from "~/components/ui/label"; import { Switch } from "~/components/ui/switch"; -import Screen from "~/components/Screen"; +import { isBiometricSupported } from "~/lib/isBiometricSupported"; import { useAppStore } from "~/lib/state/appStore"; -import { TriangleAlert } from "~/components/Icons"; import { cn } from "~/lib/utils"; -import { Card, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; export function Security() { + const [isSupported, setIsSupported] = React.useState(null); const isEnabled = useAppStore((store) => store.isSecurityEnabled); - const isSupported = false; + + React.useEffect(() => { + async function checkBiometricSupport() { + const supported = await isBiometricSupported(); + setIsSupported(supported); + } + checkBiometricSupport(); + }, []); + return ( - {!isSupported && ( - - - - - {" "}Setup Device Security - - - To protect your wallet, please set up a phone lock in your device settings first. - - - - )} - - - - { - useAppStore.getState().setSecurityEnabled(!isEnabled); - }} - nativeID="security" /> + {isSupported === null ? ( + + - + ) : ( + <> + {!isSupported && ( + + + + + Setup Device Security + + + To protect your wallet, please set up a phone lock in your device settings first. + + + + )} + + + + { + useAppStore.getState().setSecurityEnabled(!isEnabled); + }} + nativeID="security" + /> + + + + )} ); } diff --git a/pages/settings/Settings.tsx b/pages/settings/Settings.tsx index 6825aaf..67ca840 100644 --- a/pages/settings/Settings.tsx +++ b/pages/settings/Settings.tsx @@ -13,7 +13,6 @@ import Screen from "~/components/Screen"; export function Settings() { const wallet = useAppStore((store) => store.wallets[store.selectedWalletId]); - const isBiometricSupported = useAppStore((store) => store.isBiometricSupported); const [developerCounter, setDeveloperCounter] = React.useState(0); const [developerMode, setDeveloperMode] = React.useState(false); const { colorScheme, toggleColorScheme } = useColorScheme();