mirror of
https://github.com/standardnotes/mobile.git
synced 2026-02-05 21:41:23 -05:00
feature: locking privileges
This commit is contained in:
46
src/App.tsx
46
src/App.tsx
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -23,3 +23,7 @@ export const SectionContainer = styled.View<{ last: boolean }>`
|
||||
`;
|
||||
|
||||
export const SourceContainer = styled.View``;
|
||||
|
||||
export const SessionLengthContainer = styled.View`
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
422
src/screens/Authenticate/AuthenticatePrivileges.tsx
Normal file
422
src/screens/Authenticate/AuthenticatePrivileges.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user