feature: locking privileges

This commit is contained in:
Radek Czemerys
2020-09-01 15:20:24 +02:00
parent da00ea2689
commit fae5b592fa
16 changed files with 1097 additions and 213 deletions

View File

@@ -11,6 +11,7 @@ import {
import { navigationRef } from '@Lib/NavigationService';
import { useHasEditor, useIsLocked } from '@Lib/snjsHooks';
import {
CompositeNavigationProp,
DefaultTheme,
NavigationContainer,
RouteProp,
@@ -20,6 +21,7 @@ import {
StackNavigationProp,
} from '@react-navigation/stack';
import { Authenticate } from '@Screens/Authenticate/Authenticate';
import { AuthenticatePrivileges } from '@Screens/Authenticate/AuthenticatePrivileges';
import { Compose } from '@Screens/Compose/Compose';
import { PasscodeInputModal } from '@Screens/InputModal/PasscodeInputModal';
import { TagInputModal } from '@Screens/InputModal/TagInputModal';
@@ -50,11 +52,12 @@ import DrawerLayout, {
DrawerState,
} from 'react-native-gesture-handler/DrawerLayout';
import { HeaderButtons, Item } from 'react-navigation-header-buttons';
import { Challenge } from 'snjs';
import { Challenge, PrivilegeCredential, ProtectedAction } from 'snjs';
import { ThemeContext, ThemeProvider } from 'styled-components/native';
import { ApplicationContext } from './ApplicationContext';
import {
SCREEN_AUTHENTICATE,
SCREEN_AUTHENTICATE_PRIVILEGES,
SCREEN_COMPOSE,
SCREEN_INPUT_MODAL_PASSCODE,
SCREEN_INPUT_MODAL_TAG,
@@ -86,11 +89,20 @@ type ModalStackNavigatorParamList = {
[SCREEN_AUTHENTICATE]: {
challenge: Challenge;
};
[SCREEN_AUTHENTICATE_PRIVILEGES]: {
action: ProtectedAction;
privilegeCredentials: PrivilegeCredential[];
previousScreen: string;
unlockedItemId?: string;
};
};
export type AppStackNavigationProp<
T extends keyof AppStackNavigatorParamList
> = {
navigation: StackNavigationProp<AppStackNavigatorParamList, T>;
navigation: CompositeNavigationProp<
ModalStackNavigationProp<'AppStack'>['navigation'],
StackNavigationProp<AppStackNavigatorParamList, T>
>;
route: RouteProp<AppStackNavigatorParamList, T>;
};
export type ModalStackNavigationProp<
@@ -333,8 +345,7 @@ const MainStackComponent = ({ env }: { env: 'prod' | 'dev' }) => {
</HeaderButtons>
),
headerRight: () =>
env === 'dev' ||
(__DEV__ && (
(env === 'dev' || __DEV__) && (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
@@ -346,7 +357,7 @@ const MainStackComponent = ({ env }: { env: 'prod' | 'dev' }) => {
}}
/>
</HeaderButtons>
)),
),
})}
component={Settings}
/>
@@ -441,6 +452,31 @@ const MainStackComponent = ({ env }: { env: 'prod' | 'dev' }) => {
})}
component={Authenticate}
/>
<MainStack.Screen
name={SCREEN_AUTHENTICATE_PRIVILEGES}
options={() => ({
title: 'Authenticate',
headerLeft: ({ disabled, onPress }) => (
<HeaderButtons HeaderButtonComponent={IoniconsHeaderButton}>
<Item
testID="headerButton"
disabled={disabled}
title={Platform.OS === 'ios' ? 'Cancel' : ''}
iconName={
Platform.OS === 'ios'
? undefined
: StyleKit.nameForIcon(ICON_CLOSE)
}
onPress={onPress}
/>
</HeaderButtons>
),
headerTitle: ({ children }) => {
return <HeaderTitleView title={children || ''} />;
},
})}
component={AuthenticatePrivileges}
/>
</MainStack.Navigator>
);
};

View File

@@ -1,9 +1,15 @@
import { useNavigation } from '@react-navigation/native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { AppStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
import { SCREEN_NOTES } from '@Screens/screens';
import { PRIVILEGES_UNLOCK_PAYLOAD } from '@Screens/Authenticate/AuthenticatePrivileges';
import {
SCREEN_AUTHENTICATE_PRIVILEGES,
SCREEN_COMPOSE,
SCREEN_NOTES,
} from '@Screens/screens';
import React, { useCallback, useEffect } from 'react';
import { ApplicationEvent } from 'snjs';
import { ApplicationEvent, ButtonType, ProtectedAction, SNNote } from 'snjs';
import { Editor } from './Editor';
export const useSignedIn = (
signedInCallback?: () => void,
@@ -226,7 +232,9 @@ export const useSyncStatus = () => {
setLoading(false);
updateLocalDataStatus();
} else if (eventName === ApplicationEvent.WillSync) {
setStatus('Syncing...');
if (!completedInitialSync) {
setStatus('Syncing...');
}
} else if (eventName === ApplicationEvent.CompletedFullSync) {
setStatus();
if (!completedInitialSync) {
@@ -267,3 +275,140 @@ export const useSyncStatus = () => {
() => void
];
};
export const useDeleteNoteWithPrivileges = (
note: SNNote,
onDeleteCallback: () => void,
onTrashCallback: () => void,
editor?: Editor
) => {
// Context
const application = React.useContext(ApplicationContext);
const navigation = useNavigation<
AppStackNavigationProp<typeof SCREEN_NOTES>['navigation']
>();
// State
const [deleteAction, setDeleteAction] = React.useState<'trash' | 'delete'>();
const trashNote = useCallback(async () => {
const title = 'Move to Trash';
const message = 'Are you sure you want to move this note to the trash?';
const confirmed = await application?.alertService?.confirm(
message,
title,
'Confirm',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
onTrashCallback();
}
}, [application?.alertService, onTrashCallback]);
const deleteNotePermanently = useCallback(async () => {
const title = `Delete ${note!.safeTitle()}`;
const message = 'Are you sure you want to permanently delete this nite}?';
if (editor?.isTemplateNote) {
application?.alertService!.alert(
'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.'
);
return;
}
if (note!.locked) {
application?.alertService!.alert(
"This note is locked. If you'd like to delete it, unlock it, and try again."
);
return;
}
const confirmed = await application?.alertService?.confirm(
message,
title,
'Delete',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
onDeleteCallback();
}
}, [
application?.alertService,
editor?.isTemplateNote,
note,
onDeleteCallback,
]);
const deleteNote = useCallback(
async (permanently: boolean) => {
if (
await application?.privilegesService!.actionRequiresPrivilege(
ProtectedAction.DeleteNote
)
) {
const privilegeCredentials = await application!.privilegesService!.netCredentialsForAction(
ProtectedAction.DeleteNote
);
const activeScreen = application!.getAppState().isInTabletMode
? SCREEN_NOTES
: SCREEN_COMPOSE;
setDeleteAction(permanently ? 'delete' : 'trash');
navigation.navigate(SCREEN_AUTHENTICATE_PRIVILEGES, {
action: ProtectedAction.DeleteNote,
privilegeCredentials,
unlockedItemId: note.uuid,
previousScreen: activeScreen,
});
} else {
if (permanently) {
deleteNotePermanently();
} else {
trashNote();
}
}
},
[application, deleteNotePermanently, navigation, note?.uuid, trashNote]
);
/*
* After screen is focused read if a requested privilage was unlocked
*/
useFocusEffect(
useCallback(() => {
const readPrivilegesUnlockResponse = async () => {
if (deleteAction && application?.isLaunched()) {
const activeScreen = application.getAppState().isInTabletMode
? SCREEN_NOTES
: SCREEN_COMPOSE;
const result = await application?.getValue(PRIVILEGES_UNLOCK_PAYLOAD);
if (
result &&
result.previousScreen === activeScreen &&
result.unlockedAction === ProtectedAction.DeleteNote &&
result.unlockedItemId === note.uuid
) {
setDeleteAction(undefined);
application?.removeValue(PRIVILEGES_UNLOCK_PAYLOAD);
if (deleteAction === 'trash') {
trashNote();
} else if (deleteAction === 'delete') {
deleteNotePermanently();
}
} else {
setDeleteAction(undefined);
}
}
};
readPrivilegesUnlockResponse();
}, [
application,
deleteAction,
deleteNotePermanently,
note?.uuid,
trashNote,
])
);
return [deleteNote];
};

View File

@@ -23,3 +23,7 @@ export const SectionContainer = styled.View<{ last: boolean }>`
`;
export const SourceContainer = styled.View``;
export const SessionLengthContainer = styled.View`
margin-top: 10px;
`;

View File

@@ -30,7 +30,7 @@ import {
} from './Authenticate.styled';
import {
authenticationReducer,
ChallengeValueStateType,
AuthenticationValueStateType,
findMatchingValueIndex,
getLabelForStateAndType,
getTitleForStateAndType,
@@ -65,7 +65,7 @@ export const Authenticate = ({
type: challengeType,
})),
challengeValueStates: challenge.types.map(
() => ChallengeValueStateType.WaitingTurn
() => AuthenticationValueStateType.WaitingTurn
),
},
undefined
@@ -84,8 +84,8 @@ export const Authenticate = ({
const state = challengeValueStates[index];
if (
state === ChallengeValueStateType.Locked ||
state === ChallengeValueStateType.Success
state === AuthenticationValueStateType.Locked ||
state === AuthenticationValueStateType.Success
) {
return;
}
@@ -93,7 +93,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.Pending,
state: AuthenticationValueStateType.Pending,
});
await application?.submitValuesForChallenge(challenge, [challengeValue]);
@@ -105,14 +105,14 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.Locked,
state: AuthenticationValueStateType.Locked,
});
setTimeout(() => {
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.WaitingTurn,
state: AuthenticationValueStateType.WaitingTurn,
});
}, 30 * 1000);
}, []);
@@ -140,7 +140,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.Fail,
state: AuthenticationValueStateType.Fail,
});
Alert.alert(
'Unsuccessful',
@@ -180,7 +180,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.Fail,
state: AuthenticationValueStateType.Fail,
});
Alert.alert(
'Unsuccessful',
@@ -217,7 +217,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.Fail,
state: AuthenticationValueStateType.Fail,
});
}
}
@@ -234,7 +234,7 @@ export const Authenticate = ({
const firstNotSuccessful = useMemo(
() =>
challengeValueStates.findIndex(
state => state !== ChallengeValueStateType.Success
state => state !== AuthenticationValueStateType.Success
),
[challengeValueStates]
);
@@ -283,7 +283,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: challengeValue.type,
state: ChallengeValueStateType.WaitingInput,
state: AuthenticationValueStateType.WaitingInput,
});
},
[application, authenticateBiometrics, challengeValues, firstNotSuccessful]
@@ -294,7 +294,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: value.type,
state: ChallengeValueStateType.Success,
state: AuthenticationValueStateType.Success,
});
beginAuthenticatingForNextChallengeReason(value);
},
@@ -305,7 +305,7 @@ export const Authenticate = ({
dispatch({
type: 'setState',
valueType: value.type,
state: ChallengeValueStateType.Fail,
state: AuthenticationValueStateType.Fail,
});
};
useEffect(() => {
@@ -340,7 +340,6 @@ export const Authenticate = ({
let mounted = true;
const setBiometricsAsync = async () => {
if (challenge.reason !== ChallengeReason.Migration) {
console.log('ssadasfsdfsdfsdfsf');
const hasBiometrics = await checkForBiometrics();
if (mounted) {
setSupportsBiometrics(hasBiometrics);
@@ -373,7 +372,7 @@ export const Authenticate = ({
ChallengeType.Biometric
);
const state = challengeValueStates[index];
if (state === ChallengeValueStateType.Locked) {
if (state === AuthenticationValueStateType.Locked) {
return;
}
@@ -408,8 +407,8 @@ export const Authenticate = ({
const state = challengeValueStates[index];
if (
challengeValue.type === ChallengeType.Biometric &&
(state === ChallengeValueStateType.Locked ||
state === ChallengeValueStateType.Fail)
(state === AuthenticationValueStateType.Locked ||
state === AuthenticationValueStateType.Fail)
) {
beginAuthenticatingForNextChallengeReason();
return;
@@ -447,7 +446,7 @@ export const Authenticate = ({
tinted={active}
buttonText={
challengeValue.type === ChallengeType.LocalPasscode &&
state === ChallengeValueStateType.WaitingInput
state === AuthenticationValueStateType.WaitingInput
? 'Change Keyboard'
: undefined
}
@@ -512,7 +511,7 @@ export const Authenticate = ({
const isPending = useMemo(
() =>
challengeValueStates.findIndex(
state => state === ChallengeValueStateType.Pending
state => state === AuthenticationValueStateType.Pending
) >= 0,
[challengeValueStates]
);

View File

@@ -0,0 +1,422 @@
import { ButtonCell } from '@Components/ButtonCell';
import { SectionedAccessoryTableCell } from '@Components/SectionedAccessoryTableCell';
import { SectionedTableCell } from '@Components/SectionedTableCell';
import { SectionHeader } from '@Components/SectionHeader';
import { AppStateType, PasscodeKeyboardType } from '@Lib/ApplicationState';
import { ModalStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
import { SCREEN_AUTHENTICATE_PRIVILEGES } from '@Screens/screens';
import { StyleKitContext } from '@Style/StyleKit';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
useState,
} from 'react';
import { Platform, TextInput } from 'react-native';
import {
PrivilegeCredential,
PrivilegeSessionLength,
ProtectedAction,
} from 'snjs';
import { ThemeContext } from 'styled-components/native';
import {
Container,
Input,
SectionContainer,
SessionLengthContainer,
SourceContainer,
} from './Authenticate.styled';
import {
AuthenticationValueStateType,
findMatchingPrivilegeValueIndex,
getLabelForPrivilegeLockStateAndType,
getTitleForPrivilegeLockStateAndType,
isInActiveState,
PrivilegeLockValue,
privilegesAuthenticationReducer,
} from './helpers';
type Props = ModalStackNavigationProp<typeof SCREEN_AUTHENTICATE_PRIVILEGES>;
export const PRIVILEGES_UNLOCK_PAYLOAD = 'privilegesUnlockPayload';
export const AuthenticatePrivileges = ({
route: {
params: { privilegeCredentials, action, previousScreen, unlockedItemId },
},
navigation,
}: Props) => {
// Context
const application = useContext(ApplicationContext);
const styleKit = useContext(StyleKitContext);
const theme = useContext(ThemeContext);
// State
const [keyboardType, setKeyboardType] = useState<
PasscodeKeyboardType | undefined
>(undefined);
const [sessionLengthOptions] = useState(() =>
application!.privilegesService!.getSessionLengthOptions()
);
const [selectedSessionLength, setSelectedSessionLength] = useState<
PrivilegeSessionLength
>();
const [{ privilegeValues, privilegeValueStates }, dispatch] = useReducer(
privilegesAuthenticationReducer,
{
privilegeValues: privilegeCredentials.map(type => ({
value: '',
type: type,
})),
privilegeValueStates: privilegeCredentials.map(
() => AuthenticationValueStateType.WaitingTurn
),
},
undefined
);
// Refs
const localPasscodeRef = useRef<TextInput>(null);
const accountPasswordRef = useRef<TextInput>(null);
const firstNotSuccessful = useMemo(
() =>
privilegeValueStates.findIndex(
state => state !== AuthenticationValueStateType.Success
),
[privilegeValueStates]
);
useEffect(() => {
let isMounted = true;
const getSessionLength = async () => {
const length = await application?.privilegesService!.getSelectedSessionLength();
if (isMounted) {
setSelectedSessionLength(length as PrivilegeSessionLength);
}
};
getSessionLength();
return () => {
isMounted = false;
};
}, [application]);
const beginAuthenticatingForNextAuthenticationReason = useCallback(
(completedprivilegeValue?: PrivilegeLockValue) => {
let privilegeValue;
if (completedprivilegeValue === undefined) {
privilegeValue = privilegeValues[firstNotSuccessful];
} else {
const index = findMatchingPrivilegeValueIndex(
privilegeValues,
completedprivilegeValue.type
);
if (!privilegeValues.hasOwnProperty(index + 1)) {
return;
}
privilegeValue = privilegeValues[index + 1];
}
if (privilegeValue.type === PrivilegeCredential.LocalPasscode) {
localPasscodeRef.current?.focus();
} else if (privilegeValue.type === PrivilegeCredential.AccountPassword) {
accountPasswordRef.current?.focus();
}
dispatch({
type: 'setState',
valueType: privilegeValue.type,
state: AuthenticationValueStateType.WaitingInput,
});
},
[privilegeValues, firstNotSuccessful]
);
const onInvalidValue = (value: PrivilegeLockValue) => {
dispatch({
type: 'setState',
valueType: value.type,
state: AuthenticationValueStateType.Fail,
});
};
useEffect(() => {
const removeAppStateSubscriber = application
?.getAppState()
.addStateChangeObserver(state => {
if (state === AppStateType.ResumingFromBackground) {
beginAuthenticatingForNextAuthenticationReason();
}
});
return removeAppStateSubscriber;
}, [application, beginAuthenticatingForNextAuthenticationReason]);
const onValidValue = useCallback(
(value: PrivilegeLockValue) => {
dispatch({
type: 'setState',
valueType: value.type,
state: AuthenticationValueStateType.Success,
});
beginAuthenticatingForNextAuthenticationReason(value);
},
[beginAuthenticatingForNextAuthenticationReason]
);
const validatePrivilegeValue = useCallback(
async (privilegeLockValue: PrivilegeLockValue) => {
const index = findMatchingPrivilegeValueIndex(
privilegeValues,
privilegeLockValue.type
);
const state = privilegeValueStates[index];
if (
state === AuthenticationValueStateType.Locked ||
state === AuthenticationValueStateType.Success
) {
return;
}
dispatch({
type: 'setState',
valueType: privilegeLockValue.type,
state: AuthenticationValueStateType.Pending,
});
const result = await application!.privilegesService!.authenticateAction(
action,
privilegeValues.reduce((accumulator, currentValue) => {
accumulator[currentValue.type] = currentValue.value;
return accumulator;
}, {} as Partial<Record<PrivilegeCredential, string>>)
);
if (result.success) {
await application?.privilegesService!.setSessionLength(
selectedSessionLength!
);
if (
action === ProtectedAction.ViewProtectedNotes ||
action === ProtectedAction.DeleteNote
) {
await application?.setValue(PRIVILEGES_UNLOCK_PAYLOAD, {
unlockedAction: action,
unlockedItemId,
previousScreen,
});
} else {
await application?.setValue(PRIVILEGES_UNLOCK_PAYLOAD, {
unlockedAction: action,
previousScreen,
});
}
navigation.goBack();
} else {
if (result.failedCredentials) {
result.failedCredentials.map(item => {
const lockValueIndex = findMatchingPrivilegeValueIndex(
privilegeValues,
item
);
onInvalidValue(privilegeValues[lockValueIndex]);
});
}
if (result.successfulCredentials) {
result.successfulCredentials.map(item => {
const lockValueIndex = findMatchingPrivilegeValueIndex(
privilegeValues,
item
);
onValidValue(privilegeValues[lockValueIndex]);
});
}
}
},
[
privilegeValues,
privilegeValueStates,
application,
action,
navigation,
selectedSessionLength,
unlockedItemId,
previousScreen,
onValidValue,
]
);
const checkPasscodeKeyboardType = useCallback(
async () => application?.getAppState().getPasscodeKeyboardType(),
[application]
);
useEffect(() => {
let mounted = true;
const setInitialKeyboardType = async () => {
const initialKeyboardType = await checkPasscodeKeyboardType();
if (mounted) {
setKeyboardType(initialKeyboardType);
}
};
setInitialKeyboardType();
return () => {
mounted = false;
};
}, [checkPasscodeKeyboardType]);
useEffect(() => {
beginAuthenticatingForNextAuthenticationReason();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onValueChange = (newValue: PrivilegeLockValue) => {
dispatch({
type: 'setValue',
valueType: newValue.type,
value: newValue.value,
});
};
const onSubmitPress = () => {
const privilegeValue = privilegeValues[firstNotSuccessful];
validatePrivilegeValue(privilegeValue);
};
const switchKeyboard = () => {
if (keyboardType === PasscodeKeyboardType.Default) {
setKeyboardType(PasscodeKeyboardType.Numeric);
} else if (keyboardType === PasscodeKeyboardType.Numeric) {
setKeyboardType(PasscodeKeyboardType.Default);
}
};
const isPending = useMemo(
() =>
privilegeValueStates.findIndex(
singleState => singleState === AuthenticationValueStateType.Pending
) >= 0,
[privilegeValueStates]
);
const renderAuthenticationSource = (
privilegeValue: PrivilegeLockValue,
index: number
) => {
const last = index === privilegeValues.length - 1;
const state = privilegeValueStates[index];
const active = isInActiveState(state);
const isInput =
privilegeValue.type === PrivilegeCredential.LocalPasscode ||
privilegeValue.type === PrivilegeCredential.AccountPassword;
const stateLabel = getLabelForPrivilegeLockStateAndType(
privilegeValue,
state
);
const stateTitle = getTitleForPrivilegeLockStateAndType(
privilegeValue,
state
);
return (
<SourceContainer key={privilegeValue.type}>
<SectionHeader
title={stateTitle}
subtitle={isInput ? stateLabel : undefined}
tinted={active}
buttonText={
privilegeValue.type === PrivilegeCredential.LocalPasscode &&
state === AuthenticationValueStateType.WaitingInput
? 'Change Keyboard'
: undefined
}
buttonAction={switchKeyboard}
buttonStyles={
privilegeValue.type === PrivilegeCredential.LocalPasscode
? {
color: theme.stylekitNeutralColor,
fontSize: theme.mainTextFontSize - 5,
}
: undefined
}
/>
{isInput && (
<SectionContainer last={last}>
<SectionedTableCell textInputCell={true} first={true}>
<Input
key={Platform.OS === 'android' ? keyboardType : undefined}
ref={
privilegeValue.type === PrivilegeCredential.LocalPasscode
? localPasscodeRef
: accountPasswordRef
}
placeholder={
privilegeValue.type === PrivilegeCredential.LocalPasscode
? 'Local Passcode'
: 'Account password'
}
onChangeText={text => {
onValueChange({ ...privilegeValue, value: text });
}}
value={(privilegeValue.value || '') as string}
autoCorrect={false}
autoFocus={false}
autoCapitalize={'none'}
secureTextEntry={true}
keyboardType={keyboardType}
keyboardAppearance={styleKit?.keyboardColorForActiveTheme()}
underlineColorAndroid={'transparent'}
onSubmitEditing={() => {
validatePrivilegeValue(privilegeValue);
}}
/>
</SectionedTableCell>
</SectionContainer>
)}
</SourceContainer>
);
};
return (
<Container>
{privilegeValues.map((privilegeValue, index) =>
renderAuthenticationSource(privilegeValue, index)
)}
<ButtonCell
maxHeight={45}
disabled={isPending}
title={
firstNotSuccessful === privilegeValueStates.length - 1
? 'Submit'
: 'Next'
}
bold={true}
onPress={onSubmitPress}
/>
<SessionLengthContainer>
<SectionHeader title={'Remember For'} />
{sessionLengthOptions.map((option, index) => (
<SectionedAccessoryTableCell
text={option.label}
key={option.value}
first={index === 0}
last={index === sessionLengthOptions.length - 1}
selected={() => {
return option.value === selectedSessionLength;
}}
onPress={() => {
setSelectedSessionLength(option.value);
}}
/>
))}
</SessionLengthContainer>
</Container>
);
};

View File

@@ -1,4 +1,4 @@
import { ChallengeType, ChallengeValue } from 'snjs';
import { ChallengeType, ChallengeValue, PrivilegeCredential } from 'snjs';
export const findMatchingValueIndex = (
values: ChallengeValue[],
@@ -7,11 +7,18 @@ export const findMatchingValueIndex = (
return values.findIndex(arrayValue => type === arrayValue.type);
};
export const isInActiveState = (state: ChallengeValueStateType) =>
state !== ChallengeValueStateType.WaitingInput &&
state !== ChallengeValueStateType.Success;
export const findMatchingPrivilegeValueIndex = (
values: PrivilegeLockValue[],
type: PrivilegeCredential
) => {
return values.findIndex(arrayValue => type === arrayValue.type);
};
export enum ChallengeValueStateType {
export const isInActiveState = (state: AuthenticationValueStateType) =>
state !== AuthenticationValueStateType.WaitingInput &&
state !== AuthenticationValueStateType.Success;
export enum AuthenticationValueStateType {
WaitingTurn = 0,
WaitingInput = 1,
Success = 2,
@@ -22,18 +29,72 @@ export enum ChallengeValueStateType {
type ChallengeValueState = {
challengeValues: ChallengeValue[];
challengeValueStates: ChallengeValueStateType[];
challengeValueStates: AuthenticationValueStateType[];
};
type SetChallengeValueState = {
type: 'setState';
valueType: ChallengeValue['type'];
state: ChallengeValueStateType;
state: AuthenticationValueStateType;
};
type SetChallengeValue = {
type: 'setValue';
valueType: ChallengeValue['type'];
value: ChallengeValue['value'];
};
export type PrivilegeLockValue = {
type: PrivilegeCredential;
value: string;
};
type SetPrivilegesValueState = {
type: 'setState';
valueType: PrivilegeLockValue['type'];
state: AuthenticationValueStateType;
};
type SetPrivilegesValue = {
type: 'setValue';
valueType: PrivilegeLockValue['type'];
value: PrivilegeLockValue['value'];
};
type PrivilegeValueState = {
privilegeValues: PrivilegeLockValue[];
privilegeValueStates: AuthenticationValueStateType[];
};
type PrivilegesAction = SetPrivilegesValueState | SetPrivilegesValue;
export const privilegesAuthenticationReducer = (
state: PrivilegeValueState,
action: PrivilegesAction
): PrivilegeValueState => {
switch (action.type) {
case 'setState': {
const tempArray = state.privilegeValueStates.slice();
const index = findMatchingPrivilegeValueIndex(
state.privilegeValues,
action.valueType
);
tempArray[index] = action.state;
return { ...state, privilegeValueStates: tempArray };
}
case 'setValue': {
const tempArray = state.privilegeValues.slice();
const index = findMatchingPrivilegeValueIndex(
state.privilegeValues,
action.valueType
);
tempArray[index] = {
type: state.privilegeValues[index].type,
value: action.value,
};
return { ...state, privilegeValues: tempArray };
}
default:
return state;
}
};
type Action = SetChallengeValueState | SetChallengeValue;
export const authenticationReducer = (
state: ChallengeValueState,
@@ -66,28 +127,63 @@ export const authenticationReducer = (
}
};
const mapPrivilageCredentialToChallengeType = (
credential: PrivilegeCredential
) => {
switch (credential) {
case PrivilegeCredential.AccountPassword:
return ChallengeType.AccountPassword;
case PrivilegeCredential.LocalPasscode:
return ChallengeType.LocalPasscode;
}
};
export const getTitleForPrivilegeLockStateAndType = (
privilegeValue: PrivilegeLockValue,
state: AuthenticationValueStateType
) =>
getTitleForStateAndType(
{
...privilegeValue,
type: mapPrivilageCredentialToChallengeType(privilegeValue.type),
},
state
);
export const getLabelForPrivilegeLockStateAndType = (
privilegeValue: PrivilegeLockValue,
state: AuthenticationValueStateType
) =>
getLabelForStateAndType(
{
...privilegeValue,
type: mapPrivilageCredentialToChallengeType(privilegeValue.type),
},
state
);
export const getTitleForStateAndType = (
challengeValue: ChallengeValue,
state: ChallengeValueStateType
state: AuthenticationValueStateType
) => {
switch (challengeValue.type) {
case ChallengeType.AccountPassword: {
const title = 'Account Password';
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingTurn:
return title.concat(' ', '- Waiting.');
case ChallengeValueStateType.Locked:
case AuthenticationValueStateType.Locked:
return title.concat(' ', '- Locked.');
default:
return title;
}
}
case ChallengeType.LocalPasscode: {
const title = 'Local Password';
const title = 'Local Passcode';
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingTurn:
return title.concat(' ', '- Waiting.');
case ChallengeValueStateType.Locked:
case AuthenticationValueStateType.Locked:
return title.concat(' ', '- Locked.');
default:
return title;
@@ -96,9 +192,9 @@ export const getTitleForStateAndType = (
case ChallengeType.Biometric: {
const title = 'Biometrics';
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingTurn:
return title.concat(' ', '- Waiting.');
case ChallengeValueStateType.Locked:
case AuthenticationValueStateType.Locked:
return title.concat(' ', '- Locked.');
default:
return title;
@@ -109,19 +205,19 @@ export const getTitleForStateAndType = (
export const getLabelForStateAndType = (
challengeValue: ChallengeValue,
state: ChallengeValueStateType
state: AuthenticationValueStateType
) => {
switch (challengeValue.type) {
case ChallengeType.AccountPassword: {
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case ChallengeValueStateType.WaitingInput:
case AuthenticationValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingInput:
return 'Enter your account password';
case ChallengeValueStateType.Pending:
case AuthenticationValueStateType.Pending:
return 'Verifying keys...';
case ChallengeValueStateType.Success:
case AuthenticationValueStateType.Success:
return 'Success | Account Password';
case ChallengeValueStateType.Fail:
case AuthenticationValueStateType.Fail:
return 'Invalid account password. Please try again.';
default:
return '';
@@ -129,31 +225,31 @@ export const getLabelForStateAndType = (
}
case ChallengeType.LocalPasscode: {
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case ChallengeValueStateType.WaitingInput:
case AuthenticationValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingInput:
return 'Enter your local passcode';
case ChallengeValueStateType.Pending:
case AuthenticationValueStateType.Pending:
return 'Verifying keys...';
case ChallengeValueStateType.Success:
case AuthenticationValueStateType.Success:
return 'Success | Local Passcode';
case ChallengeValueStateType.Fail:
return 'Invalid local password. Please try again.';
case AuthenticationValueStateType.Fail:
return 'Invalid local passcode. Please try again.';
default:
return '';
}
}
case ChallengeType.Biometric: {
switch (state) {
case ChallengeValueStateType.WaitingTurn:
case ChallengeValueStateType.WaitingInput:
case AuthenticationValueStateType.WaitingTurn:
case AuthenticationValueStateType.WaitingInput:
return 'Please use biometrics to unlock.';
case ChallengeValueStateType.Pending:
case AuthenticationValueStateType.Pending:
return 'Waiting for unlock.';
case ChallengeValueStateType.Success:
case AuthenticationValueStateType.Success:
return 'Success | Biometrics.';
case ChallengeValueStateType.Fail:
case AuthenticationValueStateType.Fail:
return 'Biometrics failed. Tap to try again.';
case ChallengeValueStateType.Locked:
case AuthenticationValueStateType.Locked:
return 'Biometrics locked. Try again in 30 seconds.';
default:
return '';

View File

@@ -1,3 +1,4 @@
import { useDeleteNoteWithPrivileges } from '@Lib/snjsHooks';
import { ApplicationContext } from '@Root/ApplicationContext';
import {
CustomActionSheetOption,
@@ -5,13 +6,7 @@ import {
} from '@Style/useCustomActionSheet';
import React, { useCallback, useContext, useRef, useState } from 'react';
import { View } from 'react-native';
import {
ButtonType,
CollectionSort,
isNullOrUndefined,
NoteMutator,
SNNote,
} from 'snjs';
import { CollectionSort, isNullOrUndefined, NoteMutator, SNNote } from 'snjs';
import {
Container,
DateText,
@@ -59,6 +54,19 @@ export const NoteCell = ({
const { showActionSheet } = useCustomActionSheet();
const [deleteNote] = useDeleteNoteWithPrivileges(
note,
async () => {
await application?.deleteItem(note);
},
() => {
changeNote(mutator => {
mutator.trashed = true;
});
},
undefined
);
const highlight = Boolean(selected || highlighted);
const _onPress = () => {
@@ -171,24 +179,7 @@ export const NoteCell = ({
text: 'Move to Trash',
key: 'trash',
destructive: true,
callback: async () => {
const title = 'Move to Trash';
const message =
'Are you sure you want to move this note to the trash?';
const confirmed = await application?.alertService?.confirm(
message,
title,
'Confirm',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
changeNote(mutator => {
mutator.trashed = true;
});
}
},
callback: async () => deleteNote(false),
});
} else {
options = options.concat([
@@ -205,27 +196,7 @@ export const NoteCell = ({
text: 'Delete Permanently',
key: 'delete-forever',
destructive: true,
callback: async () => {
const title = `Delete ${note.safeTitle()}`;
const message =
'Are you sure you want to permanently delete this nite}?';
if (note.locked) {
application?.alertService!.alert(
"This note is locked. If you'd like to delete it, unlock it, and try again."
);
return;
}
const confirmed = await application?.alertService?.confirm(
message,
title,
'Delete',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
await application?.deleteItem(note);
}
},
callback: async () => deleteNote(true),
},
]);
}

View File

@@ -4,16 +4,28 @@ import {
TabletModeChangeData,
} from '@Lib/ApplicationState';
import { useHasEditor, useIsLocked } from '@Lib/snjsHooks';
import { useFocusEffect } from '@react-navigation/native';
import { AppStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
import { SCREEN_COMPOSE, SCREEN_NOTES } from '@Screens/screens';
import {
SCREEN_AUTHENTICATE_PRIVILEGES,
SCREEN_COMPOSE,
SCREEN_NOTES,
} from '@Screens/screens';
import { StyleKit } from '@Style/StyleKit';
import { hexToRGBA } from '@Style/utils';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { LayoutChangeEvent } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { SNNote } from 'snjs/';
import { ProtectedAction, SNNote } from 'snjs/';
import { ThemeContext } from 'styled-components/native';
import { PRIVILEGES_UNLOCK_PAYLOAD } from './Authenticate/AuthenticatePrivileges';
import { Compose } from './Compose/Compose';
import { Notes } from './Notes/Notes';
import {
@@ -45,6 +57,7 @@ export const Root = (props: Props): JSX.Element | null => {
const [keyboardHeight, setKeyboardHeight] = useState<number | undefined>(
undefined
);
const [expectsUnlock, setExpectsUnlock] = useState(false);
/**
* Register observers
@@ -123,18 +136,75 @@ export const Root = (props: Props): JSX.Element | null => {
setKeyboardHeight(application?.getAppState().getKeyboardHeight());
};
const openCompose = (newNote: boolean) => {
if (!shouldSplitLayout) {
props.navigation.navigate(SCREEN_COMPOSE, {
title: newNote ? 'Compose' : 'Note',
});
const openCompose = useCallback(
(newNote: boolean) => {
if (!shouldSplitLayout) {
props.navigation.navigate(SCREEN_COMPOSE, {
title: newNote ? 'Compose' : 'Note',
});
}
},
[props.navigation, shouldSplitLayout]
);
const openNote = useCallback(
async (noteUuid: SNNote['uuid']) => {
await application!.getAppState().openEditor(noteUuid);
openCompose(false);
},
[application, openCompose]
);
const onNoteSelect = async (noteUuid: SNNote['uuid']) => {
const note = application?.findItem(noteUuid) as SNNote;
if (note) {
if (
note.safeContent.protected &&
(await application?.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ViewProtectedNotes
))
) {
const privilegeCredentials = await application!.privilegesService!.netCredentialsForAction(
ProtectedAction.ViewProtectedNotes
);
setExpectsUnlock(true);
props.navigation.navigate(SCREEN_AUTHENTICATE_PRIVILEGES, {
action: ProtectedAction.ViewProtectedNotes,
privilegeCredentials,
unlockedItemId: noteUuid,
previousScreen: SCREEN_NOTES,
});
} else {
openNote(noteUuid);
}
}
};
const onNoteSelect = async (noteUuid: SNNote['uuid']) => {
await application!.getAppState().openEditor(noteUuid);
openCompose(false);
};
/*
* After screen is focused read if a requested privilage was unlocked
*/
useFocusEffect(
useCallback(() => {
const readPrivilegesUnlockResponse = async () => {
if (application?.isLaunched() && expectsUnlock) {
const result = await application?.getValue(PRIVILEGES_UNLOCK_PAYLOAD);
if (
result &&
result.previousScreen === SCREEN_NOTES &&
result.unlockedItemId
) {
setExpectsUnlock(false);
application?.removeValue(PRIVILEGES_UNLOCK_PAYLOAD);
openNote(result.unlockedItemId);
} else {
setExpectsUnlock(false);
}
}
};
readPrivilegesUnlockResponse();
}, [application, expectsUnlock, openNote])
);
const onNoteCreate = async () => {
await application!.getAppState().createEditor();

View File

@@ -1,6 +1,5 @@
import { ButtonCell } from '@Components/ButtonCell';
import { SectionedAccessoryTableCell } from '@Components/SectionedAccessoryTableCell';
import { SectionedTableCell } from '@Components/SectionedTableCell';
import { SectionHeader } from '@Components/SectionHeader';
import { useFocusEffect } from '@react-navigation/native';
import { ApplicationContext } from '@Root/ApplicationContext';
@@ -139,11 +138,11 @@ export const ManagePrivileges = () => {
{sessionExpirey && !sessionExpired && (
<Section>
<SectionHeader title={'Current Session'} />
<SectionedTableCell first={true}>
<StyledSectionedTableCell first={true}>
<CellText>
You will not be asked to authenticate until {sessionExpirey}.
</CellText>
</SectionedTableCell>
</StyledSectionedTableCell>
<ButtonCell
last={true}
leftAligned={true}

View File

@@ -4,13 +4,18 @@ import { SectionedOptionsTableCell } from '@Components/SectionedOptionsTableCell
import { SectionHeader } from '@Components/SectionHeader';
import { TableSection } from '@Components/TableSection';
import { useSignedIn } from '@Lib/snjsHooks';
import { useNavigation } from '@react-navigation/native';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { ModalStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
import { SCREEN_MANAGE_PRIVILEGES, SCREEN_SETTINGS } from '@Screens/screens';
import { PRIVILEGES_UNLOCK_PAYLOAD } from '@Screens/Authenticate/AuthenticatePrivileges';
import {
SCREEN_AUTHENTICATE_PRIVILEGES,
SCREEN_MANAGE_PRIVILEGES,
SCREEN_SETTINGS,
} from '@Screens/screens';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import { Alert } from 'react-native';
import { ButtonType } from 'snjs';
import { ButtonType, ProtectedAction } from 'snjs';
type Props = {
title: string;
@@ -27,6 +32,8 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
// State
const [exporting, setExporting] = useState(false);
const [awaitingUnlock, setAwaitingUnlock] = useState(false);
const [encryptedBackup, setEncryptedBackp] = useState(false);
const email = useMemo(() => {
if (signedIn) {
@@ -59,6 +66,30 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
}
};
const openPrivilegeModal = useCallback(
async (protectedAction: ProtectedAction) => {
setAwaitingUnlock(true);
const privilegeCredentials = await application!.privilegesService!.netCredentialsForAction(
protectedAction
);
navigation.navigate(SCREEN_AUTHENTICATE_PRIVILEGES, {
action: protectedAction,
privilegeCredentials,
previousScreen: SCREEN_SETTINGS,
});
},
[application, navigation]
);
const exportData = useCallback(
async (encrypted: boolean) => {
setExporting(true);
await application?.getBackupsService().export(encrypted);
setExporting(false);
},
[application]
);
const onExportPress = useCallback(
async (option: { key: string }) => {
let encrypted = option.key === 'encrypted';
@@ -70,19 +101,80 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
);
return;
}
setExporting(true);
await application?.getBackupsService().export(encrypted);
setExporting(false);
if (
await application?.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManageBackups
)
) {
setEncryptedBackp(encrypted);
await openPrivilegeModal(ProtectedAction.ManageBackups);
} else {
exportData(encrypted);
}
},
[application, encryptionAvailable]
[
application?.alertService,
application?.privilegesService,
encryptionAvailable,
exportData,
openPrivilegeModal,
]
);
const openManagePrivileges = () => {
const openManagePrivileges = useCallback(() => {
navigation.push(SCREEN_MANAGE_PRIVILEGES);
};
}, [navigation]);
const onManagePrivilegesPress = useCallback(async () => {
if (
await application?.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePrivileges
)
) {
await openPrivilegeModal(ProtectedAction.ManagePrivileges);
} else {
openManagePrivileges();
}
}, [
application?.privilegesService,
openManagePrivileges,
openPrivilegeModal,
]);
/*
* After screen is focused read if a requested privilage was unlocked
*/
useFocusEffect(
useCallback(() => {
const readPrivilegesUnlockResponse = async () => {
if (application?.isLaunched() && awaitingUnlock) {
const result = await application?.getValue(PRIVILEGES_UNLOCK_PAYLOAD);
if (result && result.previousScreen === SCREEN_SETTINGS) {
setAwaitingUnlock(false);
if (result.unlockedAction === ProtectedAction.ManagePrivileges) {
openManagePrivileges();
} else if (
result.unlockedAction === ProtectedAction.ManageBackups
) {
exportData(encryptedBackup);
setEncryptedBackp(false);
}
application?.removeValue(PRIVILEGES_UNLOCK_PAYLOAD);
} else {
setAwaitingUnlock(false);
}
}
};
readPrivilegesUnlockResponse();
}, [
application,
awaitingUnlock,
encryptedBackup,
exportData,
openManagePrivileges,
])
);
return (
<TableSection>
@@ -92,7 +184,7 @@ export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
first={true}
leftAligned={true}
title={'Manage Privileges'}
onPress={openManagePrivileges}
onPress={onManagePrivilegesPress}
/>
{signedIn && (

View File

@@ -10,9 +10,14 @@ import { MobileDeviceInterface } from '@Lib/interface';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { ModalStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
import { SCREEN_INPUT_MODAL_PASSCODE, SCREEN_SETTINGS } from '@Screens/screens';
import { PRIVILEGES_UNLOCK_PAYLOAD } from '@Screens/Authenticate/AuthenticatePrivileges';
import {
SCREEN_AUTHENTICATE_PRIVILEGES,
SCREEN_INPUT_MODAL_PASSCODE,
SCREEN_SETTINGS,
} from '@Screens/screens';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { StorageEncryptionPolicies } from 'snjs';
import { ProtectedAction, StorageEncryptionPolicies } from 'snjs';
import { Title } from './PasscodeSection.styled';
type Props = {
@@ -44,6 +49,7 @@ export const PasscodeSection = (props: Props) => {
encryptionPolictChangeInProgress,
setEncryptionPolictChangeInProgress,
] = useState(false);
const [lockedMethod, setLockedMethod] = useState<'passcode' | 'biometrics'>();
useEffect(() => {
let mounted = true;
@@ -128,26 +134,7 @@ export const PasscodeSection = (props: Props) => {
const passcodeOnPress = async () => {
if (props.hasPasscode) {
const hasAccount = Boolean(application?.hasAccount());
let message;
if (hasAccount) {
message =
'Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys.';
} else {
message =
'Are you sure you want to disable your local passcode? This will disable encryption on your data.';
}
const confirmed = await application?.alertService?.confirm(
message,
'Disable Passcode',
'Disable Passcode',
undefined
);
if (confirmed) {
await application?.removePasscode();
await application?.getAppState().setScreenshotPrivacy();
}
disableAuthentication('passcode');
} else {
navigation.push(SCREEN_INPUT_MODAL_PASSCODE);
}
@@ -169,8 +156,7 @@ export const PasscodeSection = (props: Props) => {
const onBiometricsPress = async () => {
if (hasBiometrics) {
setHasBiometrics(false);
await application?.disableBiometrics();
disableAuthentication('biometrics');
} else {
setHasBiometrics(true);
await application?.enableBiometrics();
@@ -179,6 +165,92 @@ export const PasscodeSection = (props: Props) => {
await application?.getAppState().setScreenshotPrivacy();
};
const disableBiometrics = useCallback(async () => {
setHasBiometrics(false);
await application?.disableBiometrics();
}, [application]);
const disablePasscode = useCallback(async () => {
const hasAccount = Boolean(application?.hasAccount());
let message;
if (hasAccount) {
message =
'Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys.';
} else {
message =
'Are you sure you want to disable your local passcode? This will disable encryption on your data.';
}
const confirmed = await application?.alertService?.confirm(
message,
'Disable Passcode',
'Disable Passcode',
undefined
);
if (confirmed) {
await application?.removePasscode();
await application?.getAppState().setScreenshotPrivacy();
}
}, [application]);
const disableAuthentication = useCallback(
async (authenticationMethod: 'passcode' | 'biometrics') => {
if (
await application?.privilegesService!.actionRequiresPrivilege(
ProtectedAction.ManagePasscode
)
) {
const privilegeCredentials = await application!.privilegesService!.netCredentialsForAction(
ProtectedAction.ManagePasscode
);
setLockedMethod(authenticationMethod);
navigation.navigate(SCREEN_AUTHENTICATE_PRIVILEGES, {
action: ProtectedAction.ManagePasscode,
privilegeCredentials,
previousScreen: SCREEN_SETTINGS,
});
} else {
if (authenticationMethod === 'biometrics') {
disableBiometrics();
} else if (authenticationMethod === 'passcode') {
disablePasscode();
}
}
},
[application, disableBiometrics, disablePasscode, navigation]
);
/*
* After screen is focused read if a requested privilage was unlocked
*/
useFocusEffect(
useCallback(() => {
const readPrivilegesUnlockResponse = async () => {
if (application?.isLaunched() && lockedMethod) {
const result = await application?.getValue(PRIVILEGES_UNLOCK_PAYLOAD);
if (
result &&
result.previousScreen === SCREEN_SETTINGS &&
result.unlockedAction === ProtectedAction.ManagePasscode
) {
if (lockedMethod === 'biometrics') {
disableBiometrics();
} else if (lockedMethod === 'passcode') {
disablePasscode();
}
setLockedMethod(undefined);
application?.removeValue(PRIVILEGES_UNLOCK_PAYLOAD);
application?.removeValue(PRIVILEGES_UNLOCK_PAYLOAD);
} else {
setLockedMethod(undefined);
}
}
};
readPrivilegesUnlockResponse();
}, [application, disableBiometrics, disablePasscode, lockedMethod])
);
let biometricTitle = hasBiometrics
? 'Disable Biometrics Lock'
: 'Enable Biometrics Lock';

View File

@@ -1,4 +1,5 @@
import { Editor } from '@Lib/Editor';
import { useDeleteNoteWithPrivileges } from '@Lib/snjsHooks';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { AppStackNavigationProp } from '@Root/App';
import { ApplicationContext } from '@Root/ApplicationContext';
@@ -68,6 +69,23 @@ export const NoteSideMenu = (props: Props) => {
const [selectedTags, setSelectedTags] = useState<SNTag[]>([]);
const [components, setComponents] = useState<SNComponent[]>([]);
const [deleteNote] = useDeleteNoteWithPrivileges(
note!,
async () => {
await application?.deleteItem(note!);
props.drawerRef?.closeDrawer();
if (!application?.getAppState().isInTabletMode) {
navigation.popToTop();
}
},
() => {
changeNote(mutator => {
mutator.trashed = true;
});
},
editor
);
useEffect(() => {
let mounted = true;
if (!editor && mounted) {
@@ -459,24 +477,7 @@ export const NoteSideMenu = (props: Props) => {
if (!note.safeContent.trashed) {
rawOptions.push({
text: 'Move to Trash',
onSelect: async () => {
const title = 'Move to Trash';
const message =
'Are you sure you want to move this note to the trash?';
const confirmed = await application?.alertService?.confirm(
message,
title,
'Confirm',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
changeNote(mutator => {
mutator.trashed = true;
});
}
},
onSelect: async () => deleteNote(false),
icon: ICON_TRASH,
});
}
@@ -507,37 +508,7 @@ export const NoteSideMenu = (props: Props) => {
text: 'Delete Permanently',
textClass: 'danger' as 'danger',
key: 'delete-forever',
onSelect: async () => {
const title = `Delete ${note.safeTitle()}`;
const message =
'Are you sure you want to permanently delete this nite}?';
if (editor?.isTemplateNote) {
application?.alertService!.alert(
'This note is a placeholder and cannot be deleted. To remove from your list, simply navigate to a different note.'
);
return;
}
if (note.locked) {
application?.alertService!.alert(
"This note is locked. If you'd like to delete it, unlock it, and try again."
);
return;
}
const confirmed = await application?.alertService?.confirm(
message,
title,
'Delete',
ButtonType.Danger,
'Cancel'
);
if (confirmed) {
await application?.deleteItem(note);
props.drawerRef?.closeDrawer();
if (!application?.getAppState().isInTabletMode) {
navigation.popToTop();
}
}
},
onSelect: async () => deleteNote(true),
},
{
text: 'Empty Trash',
@@ -570,7 +541,7 @@ export const NoteSideMenu = (props: Props) => {
changeNote,
leaveEditor,
application,
editor?.isTemplateNote,
deleteNote,
props.drawerRef,
navigation,
]);
@@ -640,7 +611,6 @@ export const NoteSideMenu = (props: Props) => {
buttonColor={theme.stylekitInfoColor}
iconTextColor={theme.stylekitInfoContrastColor}
onClickAction={() =>
// @ts-expect-error
navigation.navigate(SCREEN_INPUT_MODAL_TAG, { noteUuid: note.uuid })
}
visible={true}

View File

@@ -62,7 +62,8 @@ export const SideMenuHero: React.FC<Props> = props => {
async event => {
if (
event === ApplicationEvent.Launched ||
event === ApplicationEvent.SignedIn
event === ApplicationEvent.SignedIn ||
event === ApplicationEvent.WillSync
) {
setIsLocked(false);
}

View File

@@ -69,7 +69,6 @@ export const TagSelectionList = (props: Props): JSX.Element => {
{
text: 'Rename',
callback: () =>
// @ts-expect-error
navigation.navigate(SCREEN_INPUT_MODAL_TAG, { tagUuid: tag.uuid }),
},
{

View File

@@ -1,4 +1,5 @@
export const SCREEN_AUTHENTICATE = 'Authenticate';
export const SCREEN_AUTHENTICATE_PRIVILEGES = 'AuthenticatePrivileges' as 'AuthenticatePrivileges';
export const SCREEN_HOME = 'Home';
export const SCREEN_NOTES = 'Notes' as 'Notes';

View File

@@ -6741,6 +6741,13 @@ react-navigation-header-buttons@^5.0.2:
invariant ">=2"
react-native-platform-touchable "^1.1.1"
react-navigation-props-mapper@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/react-navigation-props-mapper/-/react-navigation-props-mapper-1.0.4.tgz#0c5026aeb51469f9cb9034e6dc9a4db3701e768c"
integrity sha512-uwGCzz6AYDHLEaCrACaTUrQD7e5YEUC6XFr0ooq2ZJ+tAkPQcHJqDtO3mw50FFs8KWwO37B+SBcgf9kKWyOwQQ==
dependencies:
hoist-non-react-statics "^3.3.0"
react-refresh@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.2.tgz#54a277a6caaac2803d88f1d6f13c1dcfbd81e334"