Skip to content

Commit

Permalink
feat(app): simplified auth settings
Browse files Browse the repository at this point in the history
  • Loading branch information
hbriese committed Aug 8, 2024
1 parent 8a6943d commit e03c403
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 301 deletions.
19 changes: 4 additions & 15 deletions app/src/app/(nav)/settings/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { Appbar } from '#/Appbar/Appbar';
import { AuthSettings } from '#/auth/AuthSettings';
import { ScrollableScreenSurface } from '#/layout/ScrollableScreenSurface';
import { Scrollable } from '#/Scrollable';
import { ScreenSkeleton } from '#/skeleton/ScreenSkeleton';
import { withSuspense } from '#/skeleton/withSuspense';
import { createStyles } from '@theme/styles';

function AuthSettingsScreen() {
return (
<>
<Scrollable>
<Appbar mode="large" leading="menu" headline="Authentication" />

<ScrollableScreenSurface contentContainerStyle={styles.surface}>
<AuthSettings />
</ScrollableScreenSurface>
</>
<AuthSettings />
</Scrollable>
);
}

const styles = createStyles({
surface: {
flexGrow: 1,
paddingTop: 8,
},
});

export default withSuspense(AuthSettingsScreen, <ScreenSkeleton />);

export { ErrorBoundary } from '#/ErrorBoundary';
3 changes: 2 additions & 1 deletion app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ function Layout() {

return (
<Stack
screenOptions={{
screenOptions={{
headerShown: false,
contentStyle: styles.stackContent,
}}
>
<Stack.Screen name="index" />
<Stack.Screen
name={`(modal)`}
options={{
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form';
import { Text } from 'react-native-paper';
import { Subject } from 'rxjs';
import useAsyncEffect from 'use-async-effect';
import { usePasswordHash } from '#/auth/PasswordSettingsCard';
import { usePasswordHash } from '#/auth/PasswordSettings';
import { Button } from '#/Button';
import { FormTextField } from '#/fields/FormTextField';
import { Actions } from '#/layout/Actions';
Expand Down
134 changes: 67 additions & 67 deletions app/src/components/auth/AuthSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,106 +1,106 @@
import { useImmerAtom } from 'jotai-immer';
import { Switch, Text } from 'react-native-paper';
import { Divider, Switch, Text } from 'react-native-paper';
import { ListItem } from '#/list/ListItem';
import { FingerprintIcon, LockOpenIcon, OutboundIcon } from '@theme/icons';
import { useEffect } from 'react';
import { useBiometrics } from '~/hooks/useBiometrics';
import { persistedAtom } from '~/lib/persistedAtom';
import { useAtomValue } from 'jotai';
import { SECURE_STORE_PASSWORD_ENCRYPTED as ALWAYS_REQUIRED_ON_OPEN } from '~/lib/secure-storage';
import { createStyles } from '@theme/styles';
import { ICON_SIZE } from '@theme/paper';
import { PasswordSettingsCard } from './PasswordSettingsCard';
import { PasswordSettings, usePasswordHash } from './PasswordSettings';
import { StyleProp, View, ViewStyle } from 'react-native';
import { Card } from '#/layout/Card';
import _ from 'lodash';

// Security note: this has weak security guarantees as an attacker with local access may change these settings, or even the whole JS bundle...
const AUTH_SETTINGS = persistedAtom('AuthenticationSettings', {
open: true,
approval: true,
});
export const useAuthRequiredOnOpen = () =>
useAtomValue(AUTH_SETTINGS).open || ALWAYS_REQUIRED_ON_OPEN;
export const useAuthRequiredOnApproval = () => useAtomValue(AUTH_SETTINGS).approval;

export interface AuthSettings2Props {
style?: StyleProp<ViewStyle>;
export function useAuthSettings() {
const available = useAuthAvailable();

return _.mapValues(useAtomValue(AUTH_SETTINGS), (v) => v && available);
}

export function AuthSettings({ style }: AuthSettings2Props) {
function useAuthAvailable() {
const biometrics = useBiometrics();
const passwordHash = usePasswordHash();

const [settings, updateSettings] = useImmerAtom(AUTH_SETTINGS);

// Enable biometrics (if supported) when this screen is first opened
useEffect(() => {
biometrics.setEnabled((enabled) => (enabled === null ? biometrics.available : enabled));
}, [biometrics]);

return (
<View style={[styles.container, style]}>
{biometrics.available && (
<Card type="outlined" style={[styles.indent, styles.biometrics]}>
<FingerprintIcon size={ICON_SIZE.medium} />
return biometrics.available || !!passwordHash;
}

<Text variant="titleMedium" style={styles.cardHeadline}>
Biometrics
</Text>
export interface AuthSettingsProps {
style?: StyleProp<ViewStyle>;
}

<Switch value={!!biometrics.enabled} onValueChange={(v) => biometrics.setEnabled(v)} />
</Card>
)}
export function AuthSettings({ style }: AuthSettingsProps) {
const biometrics = useBiometrics();
const available = useAuthAvailable();
const unlockDisabled = !available || ALWAYS_REQUIRED_ON_OPEN;

<PasswordSettingsCard style={styles.indent} />
const settings = useAuthSettings();
const [, updateSettings] = useImmerAtom(AUTH_SETTINGS);

<View>
<Text variant="titleMedium" style={[styles.indent, styles.authOnHeader]}>
Authenticate on
</Text>
return (
<View style={style}>
<PasswordSettings style={styles.indent} />
<Divider style={[styles.divider, styles.indent]} />

{biometrics.available && (
<ListItem
leading={LockOpenIcon}
headline="Unlock app"
trailing={() => (
<Switch
value={!ALWAYS_REQUIRED_ON_OPEN && settings.open}
onValueChange={(v) => updateSettings((s) => ({ ...s, open: v }))}
/>
)}
disabled={ALWAYS_REQUIRED_ON_OPEN}
leading={FingerprintIcon}
headline="Biometrics"
supporting="Allow biometrics to be used"
trailing={
<Switch value={biometrics.enabled} onValueChange={(v) => biometrics.setEnabled(v)} />
}
/>
)}

<ListItem
leading={OutboundIcon}
headline="Approve action"
trailing={() => (
<Switch
value={settings.approval}
onValueChange={(v) => updateSettings((s) => ({ ...s, approval: v }))}
/>
)}
/>
</View>
<Text variant="labelLarge" style={[styles.indent, styles.label]}>
Required to
</Text>

<ListItem
leading={LockOpenIcon}
headline="Unlock app"
trailing={() => (
<Switch
value={settings.open}
onValueChange={(v) => updateSettings((s) => ({ ...s, open: v }))}
disabled={unlockDisabled}
/>
)}
disabled={unlockDisabled}
/>

<ListItem
leading={OutboundIcon}
headline="Approve action"
trailing={() => (
<Switch
value={settings.approval}
onValueChange={(v) => updateSettings((s) => ({ ...s, approval: v }))}
disabled={!available}
/>
)}
disabled={!available}
/>
</View>
);
}

const styles = createStyles({
container: {
gap: 8,
divider: {
marginTop: 16,
marginBottom: 8,
},
indent: {
marginHorizontal: 16,
},
biometrics: {
flexDirection: 'row',
alignItems: 'center',
gap: 16,
padding: 16,
},
cardHeadline: {
flex: 1,
},
authOnHeader: {
marginVertical: 8,
label: {
marginTop: 16,
marginBottom: 8,
},
});
Loading

0 comments on commit e03c403

Please sign in to comment.