mobile: fix vault with biometrics enabled does not lock note

This commit is contained in:
Ammar Ahmed
2026-02-19 13:16:09 +05:00
parent fe143f66b7
commit 8b17495f0e
10 changed files with 362 additions and 401 deletions

View File

@@ -28,6 +28,7 @@ import { DDS } from "../../../services/device-detection";
import {
ToastManager,
Vault,
VaultRequestType,
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
@@ -58,148 +59,104 @@ import { DefaultAppStyles } from "../../../utils/styles";
import { Note, NoteContent, VAULT_ERRORS } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
type VaultDialogData = {
item: Note;
} & Partial<Omit<Vault, "item">>;
interface VaultDialogState {
visible: boolean;
wrongPassword: boolean;
loading: boolean;
note?: Note;
vault: boolean;
locked: boolean;
permanant: boolean;
goToEditor: boolean;
share: boolean;
passwordsDontMatch: boolean;
deleteNote: boolean;
focusIndex: number | null;
biometricUnlock: boolean;
isBiometryEnrolled: boolean;
isBiometryAvailable: boolean;
fingerprintAccess: boolean;
changePassword: boolean;
copyNote: boolean;
revokeFingerprintAccess: boolean;
title: string;
description: string | null;
clearVault: boolean;
deleteVault: boolean;
deleteAll: boolean;
noteLocked: boolean;
novault: boolean;
customActionTitle: string | null;
customActionParagraph: string | null;
customAction: boolean;
onUnlock?: (
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void;
}
export const VaultDialog: React.FC = () => {
const { colors } = useThemeColors();
const [state, setState] = useState<VaultDialogState>({
visible: false,
wrongPassword: false,
loading: false,
note: undefined,
vault: false,
locked: true,
permanant: false,
goToEditor: false,
share: false,
passwordsDontMatch: false,
deleteNote: false,
focusIndex: null,
biometricUnlock: false,
isBiometryEnrolled: false,
isBiometryAvailable: false,
fingerprintAccess: false,
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: strings.goToEditor(),
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false,
novault: false,
customActionTitle: null,
customActionParagraph: null,
customAction: false,
onUnlock: undefined
});
// UI State
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
const [wrongPassword, setWrongPassword] = useState(false);
const [passwordsDontMatch, setPasswordsDontMatch] = useState(false);
const [deleteAll, setDeleteAll] = useState(false);
const [biometricUnlock, setBiometricUnlock] = useState(false);
const [isBiometryAvailable, setIsBiometryAvailable] = useState(false);
const [isBiometryEnrolled, setIsBiometryEnrolled] = useState(false);
// Refs for non-UI state
const requestTypeRef = useRef<VaultRequestType | null>(null);
const noteRef = useRef<Note | undefined>(undefined);
const titleRef = useRef<string>(strings.goToEditor());
const descriptionRef = useRef<string | null>(null);
const paragraphRef = useRef<string | null>(null);
const buttonTitleRef = useRef<string | null>(null);
const positiveButtonTypeRef = useRef<"errorShade" | "transparent" | "accent">(
"transparent"
);
const customActionTitleRef = useRef<string | null>(null);
const customActionParagraphRef = useRef<string | null>(null);
const noteLockedRef = useRef(false);
const onUnlockRef = useRef<
| ((
item: Note & {
content?: NoteContent<false>;
},
password: string
) => void)
| undefined
>(undefined);
// Input refs
const passInputRef = useRef<TextInput>(null);
const confirmPassRef = useRef<TextInput>(null);
const changePassInputRef = useRef<TextInput>(null);
// Password refs
const passwordRef = useRef<string | null>(null);
const confirmPasswordRef = useRef<string | null>(null);
const newPasswordRef = useRef<string | null>(null);
const open = useCallback(async (data: VaultDialogData) => {
const open = useCallback(async (data: Vault) => {
const biometry = await BiometricService.isBiometryAvailable();
const available = !!biometry;
const fingerprint = await BiometricService.hasInternetCredentials();
const noteLocked = data.item
? await db.vaults.itemExists(data.item)
: false;
setState((prev) => ({
...prev,
note: data.item,
novault: data.novault || false,
locked: data.locked || false,
permanant: data.permanant || false,
goToEditor: data.goToEditor || false,
share: data.share || false,
deleteNote: data.deleteNote || false,
copyNote: data.copyNote || false,
isBiometryAvailable: available,
biometricUnlock: fingerprint,
isBiometryEnrolled: fingerprint,
fingerprintAccess: data.fingerprintAccess || false,
changePassword: data.changePassword || false,
revokeFingerprintAccess: data.revokeFingerprintAccess || false,
title: data.title || strings.goToEditor(),
description: data.description || null,
clearVault: data.clearVault || false,
deleteVault: data.deleteVault || false,
noteLocked,
customActionTitle: data.customActionTitle || null,
customActionParagraph: data.customActionParagraph || null,
customAction: !!(data.customActionTitle && data.customActionParagraph),
onUnlock: data.onUnlock
}));
// Set refs
noteRef.current = data.item;
titleRef.current = data.title || strings.goToEditor();
descriptionRef.current = data.description || null;
paragraphRef.current = data.paragraph || null;
buttonTitleRef.current = data.buttonTitle || null;
positiveButtonTypeRef.current = data.positiveButtonType || "transparent";
customActionTitleRef.current = data.customActionTitle || null;
customActionParagraphRef.current = data.customActionParagraph || null;
noteLockedRef.current = noteLocked;
onUnlockRef.current = data.onUnlock;
requestTypeRef.current = data.requestType;
if (
// Set UI state
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
// Auto-unlock with fingerprint if applicable
const canAutoUnlock =
fingerprint &&
data.novault &&
!data.fingerprintAccess &&
!data.revokeFingerprintAccess &&
!data.changePassword &&
!data.clearVault &&
!data.deleteVault &&
!data.customActionTitle
) {
data.requestType !== VaultRequestType.EnableFingerprint &&
data.requestType !== VaultRequestType.RevokeFingerprint &&
data.requestType !== VaultRequestType.ChangePassword &&
data.requestType !== VaultRequestType.ClearVault &&
data.requestType !== VaultRequestType.DeleteVault &&
data.requestType !== VaultRequestType.CustomAction &&
data.requestType === VaultRequestType.PermanentUnlock;
if (canAutoUnlock) {
await onPressFingerprintAuth(data.title, data.description);
} else {
setState((prev) => ({ ...prev, visible: true }));
setVisible(true);
}
}, []);
const close = useCallback(() => {
if (state.loading) {
if (loading) {
ToastManager.show({
heading: state.title,
heading: titleRef.current,
message: strings.pleaseWait() + "...",
type: "success",
context: "local"
@@ -209,46 +166,37 @@ export const VaultDialog: React.FC = () => {
Navigation.queueRoutesForUpdate();
// Reset password refs
passwordRef.current = null;
confirmPasswordRef.current = null;
newPasswordRef.current = null;
setState({
visible: false,
wrongPassword: false,
loading: false,
note: undefined,
vault: false,
locked: false,
permanant: false,
goToEditor: false,
share: false,
passwordsDontMatch: false,
deleteNote: false,
focusIndex: null,
biometricUnlock: false,
isBiometryEnrolled: false,
isBiometryAvailable: false,
fingerprintAccess: false,
changePassword: false,
copyNote: false,
revokeFingerprintAccess: false,
title: strings.goToEditor(),
description: null,
clearVault: false,
deleteVault: false,
deleteAll: false,
noteLocked: false,
novault: false,
customActionTitle: null,
customActionParagraph: null,
customAction: false,
onUnlock: undefined
});
}, [state.loading, state.title]);
// Reset refs
requestTypeRef.current = null;
noteRef.current = undefined;
titleRef.current = strings.goToEditor();
descriptionRef.current = null;
paragraphRef.current = null;
buttonTitleRef.current = null;
positiveButtonTypeRef.current = "transparent";
customActionTitleRef.current = null;
customActionParagraphRef.current = null;
noteLockedRef.current = false;
onUnlockRef.current = undefined;
// Reset UI state
setVisible(false);
setLoading(false);
setWrongPassword(false);
setPasswordsDontMatch(false);
setDeleteAll(false);
setBiometricUnlock(false);
setIsBiometryAvailable(false);
setIsBiometryEnrolled(false);
}, [loading]);
const deleteVault = useCallback(async () => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
let verified = true;
if (await db.user.getUser()) {
@@ -256,7 +204,7 @@ export const VaultDialog: React.FC = () => {
}
if (verified) {
let noteIds: string[] = [];
if (state.deleteAll) {
if (deleteAll) {
const vault = await db.vaults.default();
const relations = await db.relations
.from(
@@ -269,9 +217,9 @@ export const VaultDialog: React.FC = () => {
.get();
noteIds = relations.map((item) => item.toId);
}
await db.vault.delete(state.deleteAll);
await db.vault.delete(deleteAll);
if (state.deleteAll) {
if (deleteAll) {
noteIds.forEach((id) => {
eSendEvent(
eUpdateNoteInEditor,
@@ -284,7 +232,7 @@ export const VaultDialog: React.FC = () => {
});
}
eSendEvent("vaultUpdated");
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
setTimeout(() => {
close();
}, 100);
@@ -298,10 +246,10 @@ export const VaultDialog: React.FC = () => {
} catch (e) {
console.error(e);
}
}, [state.deleteAll, close]);
}, [deleteAll, close]);
const clearVault = useCallback(async () => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
const vault = await db.vaults.default();
const relations = await db.relations.from(vault!, "note").get();
@@ -319,7 +267,7 @@ export const VaultDialog: React.FC = () => {
true
);
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
close();
eSendEvent("vaultUpdated");
} catch (e) {
@@ -329,7 +277,7 @@ export const VaultDialog: React.FC = () => {
context: "local"
});
}
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}, [close]);
const lockNote = useCallback(async () => {
@@ -341,9 +289,9 @@ export const VaultDialog: React.FC = () => {
});
return;
} else {
await db.vault.add(state.note!.id);
await db.vault.add(noteRef.current!.id);
eSendEvent(eUpdateNoteInEditor, state.note, true);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
close();
ToastManager.show({
@@ -351,26 +299,29 @@ export const VaultDialog: React.FC = () => {
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}
}, [state.note, close]);
}, [close]);
const permanantUnlock = useCallback(() => {
db.vault
.remove(state.note!.id, passwordRef.current || "")
.then(() => {
.remove(noteRef.current!.id, passwordRef.current || "")
.then(async () => {
ToastManager.show({
heading: strings.noteUnlocked(),
type: "success",
context: "global"
});
eSendEvent(eUpdateNoteInEditor, state.note, true);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
close();
})
.catch((e) => {
takeErrorAction();
});
}, [state.note, close]);
}, [close, biometricUnlock, isBiometryEnrolled]);
const openInEditor = useCallback(
(note: Note & { content?: NoteContent<false> }) => {
@@ -418,20 +369,17 @@ export const VaultDialog: React.FC = () => {
const deleteNote = useCallback(async () => {
try {
await db.vault.remove(state.note!.id, passwordRef.current || "");
await deleteItems("note", [state.note!.id]);
await db.vault.remove(noteRef.current!.id, passwordRef.current || "");
await deleteItems("note", [noteRef.current!.id]);
close();
} catch (e) {
takeErrorAction();
}
}, [state.note, close]);
}, [close]);
const takeErrorAction = useCallback(() => {
setState((prev) => ({
...prev,
wrongPassword: true,
visible: true
}));
setWrongPassword(true);
setVisible(true);
setTimeout(() => {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -445,39 +393,40 @@ export const VaultDialog: React.FC = () => {
try {
if (!passwordRef.current) throw new Error("Invalid password");
const note = await db.vault.open(state.note!.id, passwordRef.current);
const note = await db.vault.open(
noteRef.current!.id,
passwordRef.current
);
if (!note) throw new Error("Failed to unlock note.");
if (state.biometricUnlock && !state.isBiometryEnrolled) {
if (biometricUnlock && !isBiometryEnrolled) {
await enrollFingerprint(passwordRef.current || "");
}
if (state.goToEditor) {
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.GoToEditor) {
openInEditor(note);
} else if (state.share) {
} else if (requestType === VaultRequestType.ShareNote) {
await shareNote(note);
} else if (state.deleteNote) {
} else if (requestType === VaultRequestType.DeleteNote) {
await deleteNote();
} else if (state.copyNote) {
} else if (requestType === VaultRequestType.CopyNote) {
await copyNote(note);
} else if (state.customAction && state.onUnlock) {
} else if (
requestType === VaultRequestType.CustomAction &&
onUnlockRef.current
) {
const password = passwordRef.current;
close();
await sleep(300);
state.onUnlock(note, password);
onUnlockRef.current(note, password);
}
} catch (e) {
takeErrorAction();
}
}, [
state.note,
state.biometricUnlock,
state.isBiometryEnrolled,
state.goToEditor,
state.share,
state.deleteNote,
state.copyNote,
state.customAction,
state.onUnlock,
biometricUnlock,
isBiometryEnrolled,
openInEditor,
shareNote,
deleteNote,
@@ -495,20 +444,20 @@ export const VaultDialog: React.FC = () => {
});
return;
}
if (state.permanant) {
if (requestTypeRef.current === VaultRequestType.PermanentUnlock) {
permanantUnlock();
} else {
await openNote();
}
}, [state.permanant, permanantUnlock, openNote]);
}, [permanantUnlock, openNote]);
const enrollFingerprint = useCallback(
async (password: string) => {
setState((prev) => ({ ...prev, loading: true }));
setLoading(true);
try {
await db.vault.unlock(password);
await BiometricService.storeCredentials(password);
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
eSendEvent("vaultUpdated");
ToastManager.show({
heading: strings.biometricUnlockEnabled(),
@@ -523,7 +472,7 @@ export const VaultDialog: React.FC = () => {
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
}
},
[close]
@@ -532,13 +481,13 @@ export const VaultDialog: React.FC = () => {
const createVault = useCallback(async () => {
await db.vault.create(passwordRef.current || "");
if (state.biometricUnlock) {
if (biometricUnlock) {
await enrollFingerprint(passwordRef.current || "");
}
if (state.note?.id) {
await db.vault.add(state.note.id);
eSendEvent(eUpdateNoteInEditor, state.note, true);
setState((prev) => ({ ...prev, loading: false }));
if (noteRef.current?.id) {
await db.vault.add(noteRef.current.id);
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
setLoading(false);
ToastManager.show({
heading: strings.noteLocked(),
type: "success",
@@ -554,7 +503,7 @@ export const VaultDialog: React.FC = () => {
close();
}
eSendEvent("vaultUpdated");
}, [state.biometricUnlock, state.note, enrollFingerprint, close]);
}, [biometricUnlock, enrollFingerprint, close]);
const revokeFingerprintAccess = useCallback(async () => {
try {
@@ -578,34 +527,38 @@ export const VaultDialog: React.FC = () => {
async (title?: string, description?: string) => {
try {
const credentials = await BiometricService.getCredentials(
title || state.title,
description || state.description || ""
title || titleRef.current,
description || descriptionRef.current || ""
);
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
passwordRef.current = credentials.password;
console.log("password...");
onPress();
} else {
eSendEvent(eCloseActionSheet);
await sleep(300);
setState((prev) => ({ ...prev, visible: true }));
setVisible(true);
}
} catch (e) {
console.error(e);
}
},
[state.title, state.description]
[]
);
const onPress = useCallback(async () => {
if (state.revokeFingerprintAccess) {
const requestType = requestTypeRef.current;
if (requestType === VaultRequestType.RevokeFingerprint) {
await revokeFingerprintAccess();
close();
return;
}
if (state.loading) return;
if (loading) return;
if (!passwordRef.current) {
ToastManager.show({
@@ -616,26 +569,26 @@ export const VaultDialog: React.FC = () => {
return;
}
if (!state.novault) {
if (requestType === VaultRequestType.CreateVault) {
if (passwordRef.current !== confirmPasswordRef.current) {
ToastManager.show({
heading: strings.passwordNotMatched(),
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
setPasswordsDontMatch(true);
return;
}
createVault();
} else if (state.changePassword) {
setState((prev) => ({ ...prev, loading: true }));
} else if (requestType === VaultRequestType.ChangePassword) {
setLoading(true);
db.vault
.changePassword(passwordRef.current, newPasswordRef.current || "")
.then(() => {
setState((prev) => ({ ...prev, loading: false }));
if (state.biometricUnlock) {
setLoading(false);
if (biometricUnlock) {
enrollFingerprint(newPasswordRef.current || "");
}
ToastManager.show({
@@ -646,7 +599,7 @@ export const VaultDialog: React.FC = () => {
close();
})
.catch((e) => {
setState((prev) => ({ ...prev, loading: false }));
setLoading(false);
if (e.message === VAULT_ERRORS.wrongPassword) {
ToastManager.show({
heading: strings.passwordIncorrect(),
@@ -657,50 +610,62 @@ export const VaultDialog: React.FC = () => {
ToastManager.error(e);
}
});
} else if (state.locked) {
} else if (requestType === VaultRequestType.LockNote) {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setState((prev) => ({ ...prev, wrongPassword: true }));
setWrongPassword(true);
return;
}
if (state.noteLocked) {
db.vault
.unlock(passwordRef.current)
.then(async (unlocked) => {
if (unlocked) {
setWrongPassword(false);
await lockNote();
} else {
takeErrorAction();
}
})
.catch((e) => {
takeErrorAction();
});
} else if (
requestType === VaultRequestType.UnlockNote ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.ShareNote ||
requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
requestType === VaultRequestType.CustomAction
) {
if (!passwordRef.current || passwordRef.current.trim() === "") {
ToastManager.show({
heading: strings.passwordIncorrect(),
type: "error",
context: "local"
});
setWrongPassword(true);
return;
}
if (noteLockedRef.current) {
await unlockNote();
} else {
db.vault
.unlock(passwordRef.current)
.then(async () => {
setState((prev) => ({ ...prev, wrongPassword: false }));
await lockNote();
})
.catch((e) => {
takeErrorAction();
});
console.log("Error: Note should be locked for this operation");
}
} else if (state.fingerprintAccess) {
} else if (requestType === VaultRequestType.EnableFingerprint) {
enrollFingerprint(passwordRef.current);
} else if (state.clearVault) {
} else if (requestType === VaultRequestType.ClearVault) {
await clearVault();
} else if (state.deleteVault) {
} else if (requestType === VaultRequestType.DeleteVault) {
await deleteVault();
} else if (state.customAction) {
await unlockNote();
}
}, [
state.revokeFingerprintAccess,
state.loading,
state.novault,
state.changePassword,
state.locked,
state.noteLocked,
state.fingerprintAccess,
state.clearVault,
state.deleteVault,
state.customAction,
state.biometricUnlock,
loading,
biometricUnlock,
revokeFingerprintAccess,
close,
createVault,
@@ -722,23 +687,21 @@ export const VaultDialog: React.FC = () => {
};
}, [open, close]);
if (!state.visible) return null;
if (!visible) return null;
const {
note,
novault,
deleteNote: shouldDeleteNote,
share,
goToEditor,
fingerprintAccess,
changePassword,
loading,
deleteVault: shouldDeleteVault,
clearVault: shouldClearVault,
customAction,
customActionTitle,
customActionParagraph
} = state;
const requestType = requestTypeRef.current;
const isCreateVault = requestType === VaultRequestType.CreateVault;
const isChangePassword = requestType === VaultRequestType.ChangePassword;
const isClearVault = requestType === VaultRequestType.ClearVault;
const isDeleteVault = requestType === VaultRequestType.DeleteVault;
const isRevokeFingerprint =
requestType === VaultRequestType.RevokeFingerprint;
const isEnableFingerprint =
requestType === VaultRequestType.EnableFingerprint;
const isCustomAction = requestType === VaultRequestType.CustomAction;
const isDeleteNote = requestType === VaultRequestType.DeleteNote;
const isShareNote = requestType === VaultRequestType.ShareNote;
const isGoToEditor = requestType === VaultRequestType.GoToEditor;
return (
<BaseDialog
@@ -760,8 +723,10 @@ export const VaultDialog: React.FC = () => {
}}
>
<DialogHeader
title={state.title}
paragraph={customActionParagraph || ""}
title={titleRef.current}
paragraph={
paragraphRef.current || customActionParagraphRef.current || ""
}
icon="shield"
padding={12}
/>
@@ -772,12 +737,12 @@ export const VaultDialog: React.FC = () => {
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{(novault ||
changePassword ||
shouldClearVault ||
shouldDeleteVault ||
customAction) &&
!state.revokeFingerprintAccess ? (
{(isChangePassword ||
isClearVault ||
!isCreateVault ||
isDeleteVault ||
isCustomAction) &&
!isRevokeFingerprint ? (
<>
<Input
fwdRef={passInputRef}
@@ -788,37 +753,39 @@ export const VaultDialog: React.FC = () => {
passwordRef.current = value;
}}
marginBottom={
!state.biometricUnlock ||
!state.isBiometryEnrolled ||
!novault ||
changePassword ||
customAction
!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction
? 0
: 10
}
onSubmit={() => {
if (changePassword) {
if (isChangePassword) {
confirmPassRef.current?.focus();
} else {
onPress();
}
}}
autoComplete="password"
returnKeyLabel={changePassword ? strings.next() : state.title}
returnKeyType={changePassword ? "next" : "done"}
returnKeyLabel={
isChangePassword ? strings.next() : titleRef.current
}
returnKeyType={isChangePassword ? "next" : "done"}
secureTextEntry
placeholder={
changePassword
isChangePassword
? strings.currentPassword()
: strings.password()
}
/>
{!state.biometricUnlock ||
!state.isBiometryEnrolled ||
!novault ||
changePassword ||
customAction ? null : (
{!biometricUnlock ||
!isBiometryEnrolled ||
isCreateVault ||
isChangePassword ||
isCustomAction ? null : (
<Button
onPress={() =>
onPressFingerprintAuth(strings.unlockNote(), "")
@@ -832,16 +799,11 @@ export const VaultDialog: React.FC = () => {
</>
) : null}
{shouldDeleteVault && (
{isDeleteVault && (
<Button
onPress={() =>
setState((prev) => ({
...prev,
deleteAll: !prev.deleteAll
}))
}
onPress={() => setDeleteAll(!deleteAll)}
icon={
state.deleteAll
deleteAll
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
@@ -854,7 +816,7 @@ export const VaultDialog: React.FC = () => {
/>
)}
{changePassword ? (
{isChangePassword ? (
<>
<Seperator half />
<Input
@@ -877,7 +839,7 @@ export const VaultDialog: React.FC = () => {
</>
) : null}
{!novault ? (
{isCreateVault ? (
<View>
<Input
fwdRef={passInputRef}
@@ -912,12 +874,9 @@ export const VaultDialog: React.FC = () => {
onChangeText={(value) => {
confirmPasswordRef.current = value;
if (value !== passwordRef.current) {
setState((prev) => ({ ...prev, passwordsDontMatch: true }));
setPasswordsDontMatch(true);
} else {
setState((prev) => ({
...prev,
passwordsDontMatch: false
}));
setPasswordsDontMatch(false);
}
}}
onSubmit={() => {
@@ -928,22 +887,20 @@ export const VaultDialog: React.FC = () => {
</View>
) : null}
{state.biometricUnlock && !state.isBiometryEnrolled && novault ? (
{biometricUnlock && !isBiometryEnrolled && !isCreateVault ? (
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
) : null}
{state.isBiometryAvailable &&
!state.fingerprintAccess &&
!shouldClearVault &&
!shouldDeleteVault &&
!customAction &&
((!state.biometricUnlock && !changePassword) || !novault) ? (
{requestType === VaultRequestType.CopyNote ||
requestType === VaultRequestType.DeleteNote ||
requestType === VaultRequestType.ShareNote ||
requestType === VaultRequestType.CustomAction ||
requestType === VaultRequestType.GoToEditor ||
requestType === VaultRequestType.PermanentUnlock ||
requestType === VaultRequestType.LockNote ? (
<Button
onPress={() => {
setState((prev) => ({
...prev,
biometricUnlock: !prev.biometricUnlock
}));
setBiometricUnlock(!biometricUnlock);
}}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
@@ -952,11 +909,9 @@ export const VaultDialog: React.FC = () => {
width="100%"
title={strings.unlockWithBiometrics()}
iconColor={
state.biometricUnlock
? colors.selected.accent
: colors.primary.icon
biometricUnlock ? colors.selected.accent : colors.primary.icon
}
type={state.biometricUnlock ? "transparent" : "plain"}
type={biometricUnlock ? "transparent" : "plain"}
/>
) : null}
</View>
@@ -965,34 +920,8 @@ export const VaultDialog: React.FC = () => {
onPressNegative={close}
onPressPositive={onPress}
loading={loading}
positiveType={
shouldDeleteVault || shouldClearVault ? "errorShade" : "transparent"
}
positiveTitle={
shouldDeleteVault
? strings.delete()
: shouldClearVault
? strings.clear()
: fingerprintAccess
? strings.enable()
: state.revokeFingerprintAccess
? strings.revoke()
: changePassword
? strings.change()
: customAction && customActionTitle
? customActionTitle
: state.noteLocked
? shouldDeleteNote
? strings.delete()
: share
? strings.share()
: goToEditor
? strings.open()
: strings.unlock()
: !note?.id
? strings.create()
: strings.lock()
}
positiveType={positiveButtonTypeRef.current}
positiveTitle={buttonTitleRef.current || strings.unlock()}
/>
</View>
<Toast context="local" />

View File

@@ -41,7 +41,8 @@ import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent,
openVault
openVault,
VaultRequestType
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Sync from "../../services/sync";
@@ -128,10 +129,12 @@ const MergeConflicts = () => {
let noteContent: UnencryptedContentItem;
if (isLocked) {
openVault({
requestType: VaultRequestType.CustomAction,
item: item,
novault: true,
customActionTitle: "Unlock note",
customActionParagraph: "Unlock note to merge conflicts",
title: strings.unlockNote(),
customActionTitle: strings.unlockNote(),
customActionParagraph: strings.unlockNoteToMergeConflicts(),
buttonTitle: strings.unlock(),
onUnlock: async (item, password) => {
if (!item || !password) return;
const currentContent = await db.content.get(item.contentId!);

View File

@@ -54,7 +54,8 @@ import {
eSubscribeEvent,
openVault,
presentSheet,
ToastManager
ToastManager,
VaultRequestType
} from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
@@ -408,12 +409,12 @@ export const useActions = ({
if (item.type === "note" && (await db.vaults.itemExists(item))) {
openVault({
deleteNote: true,
novault: true,
locked: true,
requestType: VaultRequestType.DeleteNote,
item: item,
title: strings.deleteNote(),
description: strings.unlockToDelete()
description: strings.unlockToDelete(),
buttonTitle: strings.delete(),
positiveButtonType: "errorShade"
});
} else {
try {
@@ -867,11 +868,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
requestType: VaultRequestType.ShareNote,
item: item,
novault: true,
locked: true,
share: true,
title: strings.shareNote()
title: strings.shareNote(),
buttonTitle: strings.share()
});
} else {
processingId.current = "shareNote";
@@ -896,11 +896,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
requestType: VaultRequestType.PermanentUnlock,
item: item,
novault: true,
locked: true,
permanant: true,
title: strings.unlockNote()
title: strings.unlockNote(),
buttonTitle: strings.unlock()
});
return;
}
@@ -918,17 +917,18 @@ export const useActions = ({
switch ((e as Error).message) {
case VAULT_ERRORS.noVault:
openVault({
requestType: VaultRequestType.CreateVault,
item: item,
novault: false,
title: strings.createVault()
title: strings.createVault(),
buttonTitle: strings.lock()
});
break;
case VAULT_ERRORS.vaultLocked:
openVault({
requestType: VaultRequestType.LockNote,
item: item,
novault: true,
locked: true,
title: strings.lockNote()
title: strings.lockNote(),
buttonTitle: strings.lock()
});
break;
}
@@ -949,11 +949,10 @@ export const useActions = ({
close();
await sleep(300);
openVault({
copyNote: true,
novault: true,
locked: true,
requestType: VaultRequestType.CopyNote,
item: item as Note,
title: strings.copyNote()
title: strings.copyNote(),
buttonTitle: strings.copy()
});
} else {
processingId.current = "copyContent";

View File

@@ -61,6 +61,7 @@ const Home = ({
/>
<DelayLayout type="settings">
<LegendList
testID="settings-list"
data={settingsGroups}
keyExtractor={keyExtractor}
renderItem={renderItem}

View File

@@ -53,7 +53,8 @@ import {
eSendEvent,
eSubscribeEvent,
openVault,
presentSheet
presentSheet,
VaultRequestType
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import Notifications from "../../services/notifications";
@@ -967,8 +968,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => (current as VaultStatusType)?.exists,
modifer: () => {
openVault({
novault: false,
title: strings.createVault()
requestType: VaultRequestType.CreateVault,
title: strings.createVault(),
buttonTitle: strings.create()
});
}
},
@@ -980,9 +982,9 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () =>
openVault({
changePassword: true,
novault: true,
title: strings.changeVaultPassword()
requestType: VaultRequestType.ChangePassword,
title: strings.changeVaultPassword(),
buttonTitle: strings.change()
})
},
{
@@ -993,9 +995,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
clearVault: true,
novault: true,
title: strings.clearVault() + "?"
requestType: VaultRequestType.ClearVault,
title: strings.clearVault() + "?",
buttonTitle: strings.clear(),
positiveButtonType: "errorShade"
});
}
},
@@ -1007,9 +1010,10 @@ export const settingsGroups: SettingSection[] = [
hidden: (current) => !(current as VaultStatusType)?.exists,
modifer: () => {
openVault({
deleteVault: true,
novault: true,
title: strings.deleteVault() + "?"
requestType: VaultRequestType.DeleteVault,
title: strings.deleteVault() + "?",
buttonTitle: strings.delete(),
positiveButtonType: "errorShade"
});
}
},
@@ -1027,13 +1031,15 @@ export const settingsGroups: SettingSection[] = [
getter: (current) => (current as VaultStatusType)?.biometryEnrolled,
modifer: (current) => {
const _current = current as VaultStatusType;
const isRevoking = _current.biometryEnrolled;
openVault({
fingerprintAccess: !_current.biometryEnrolled,
revokeFingerprintAccess: _current.biometryEnrolled,
novault: true,
title: _current.biometryEnrolled
requestType: isRevoking
? VaultRequestType.RevokeFingerprint
: VaultRequestType.EnableFingerprint,
title: isRevoking
? strings.revokeBiometricUnlock()
: strings.vaultEnableBiometrics()
: strings.vaultEnableBiometrics(),
buttonTitle: isRevoking ? strings.revoke() : strings.enable()
});
}
}

View File

@@ -32,25 +32,34 @@ import {
} from "../utils/events";
import { strings } from "@notesnook/intl";
export enum VaultRequestType {
CreateVault = "createVault",
LockNote = "lockNote",
UnlockNote = "unlockNote",
PermanentUnlock = "permanentUnlock",
GoToEditor = "goToEditor",
ShareNote = "shareNote",
CopyNote = "copyNote",
DeleteNote = "deleteNote",
EnableFingerprint = "enableFingerprint",
RevokeFingerprint = "revokeFingerprint",
ChangePassword = "changePassword",
ClearVault = "clearVault",
DeleteVault = "deleteVault",
CustomAction = "customAction"
}
export type Vault = {
item: Note;
novault: boolean;
title: string;
description: string;
locked: boolean;
permanant: boolean;
goToEditor: boolean;
share: boolean;
deleteNote: boolean;
fingerprintAccess: boolean;
revokeFingerprintAccess: boolean;
changePassword: boolean;
clearVault: boolean;
deleteVault: boolean;
copyNote: boolean;
customActionTitle: string;
customActionParagraph: string;
onUnlock: (
item?: Note;
requestType: VaultRequestType;
title?: string;
description?: string;
paragraph?: string;
buttonTitle?: string;
positiveButtonType?: "errorShade" | "transparent" | "accent";
customActionTitle?: string;
customActionParagraph?: string;
onUnlock?: (
item: Note & {
content?: NoteContent<false>;
},
@@ -87,7 +96,7 @@ export const eSendEvent = (eventName: string, ...args: any[]) => {
eventManager.publish(eventName, ...args);
};
export const openVault = (data: Partial<Vault>) => {
export const openVault = (data: Vault) => {
eSendEvent(eOpenVaultDialog, data);
};

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { expect } from "detox";
import { notesnook } from "../test.ids";
import { TestBuilder, Tests } from "./utils";
import { Element, TestBuilder, Tests } from "./utils";
async function lockNote() {
await TestBuilder.create()
@@ -67,6 +67,11 @@ async function goToPrivacySecuritySettings() {
.waitAndTapById("sidemenu-settings-icon")
.wait()
.waitAndTapByText("Settings")
.addStep(async () => {
const element = new Element("id", "settings-list");
await element.element.scroll(200, "down");
})
.wait(500)
.waitAndTapByText("Vault")
.run();
}

View File

@@ -6783,6 +6783,10 @@ msgstr "Unlock note"
msgid "Unlock note to delete it"
msgstr "Unlock note to delete it"
#: src/strings.ts:2637
msgid "Unlock note to merge conflicts"
msgstr "Unlock note to merge conflicts"
#: src/strings.ts:1208
msgid "Unlock the app with biometric authentication. This requires biometrics to be enabled on your device."
msgstr "Unlock the app with biometric authentication. This requires biometrics to be enabled on your device."

View File

@@ -6742,6 +6742,10 @@ msgstr ""
msgid "Unlock note to delete it"
msgstr ""
#: src/strings.ts:2637
msgid "Unlock note to merge conflicts"
msgstr ""
#: src/strings.ts:1208
msgid "Unlock the app with biometric authentication. This requires biometrics to be enabled on your device."
msgstr ""

View File

@@ -2633,5 +2633,6 @@ Use this if changes from other devices are not appearing on this device. This wi
exportCsv: () => t`Export CSV`,
importCsv: () => t`Import CSV`,
noContent: () => t`This note is empty`,
deleteData: () => t`Delete data`
deleteData: () => t`Delete data`,
unlockNoteToMergeConflicts: () => t`Unlock note to merge conflicts`
};