From f75ce74c6b8a2788620d5316c229a6fb0bd0a0ed Mon Sep 17 00:00:00 2001
From: kashaf-ansari-dev
Date: Wed, 3 Jun 2026 00:19:47 +0500
Subject: [PATCH 1/8] mobile: replace Input with FormInput in VaultDialog for
inline field validation
Signed-off-by: kashaf-ansari-dev
---
.../app/components/dialogs/vault/index.tsx | 230 +++++++-----------
1 file changed, 86 insertions(+), 144 deletions(-)
diff --git a/apps/mobile/app/components/dialogs/vault/index.tsx b/apps/mobile/app/components/dialogs/vault/index.tsx
index c95885e99..20edd9f1c 100644
--- a/apps/mobile/app/components/dialogs/vault/index.tsx
+++ b/apps/mobile/app/components/dialogs/vault/index.tsx
@@ -51,7 +51,10 @@ import DialogButtons from "../../dialog/dialog-buttons";
import DialogHeader from "../../dialog/dialog-header";
import { Toast } from "../../toast";
import { Button } from "../../ui/button";
-import Input from "../../ui/input";
+import FormInput, {
+ createFormRef,
+ validators
+} from "../../ui/input/form-input";
import Seperator from "../../ui/seperator";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
@@ -70,8 +73,6 @@ export const VaultDialog: React.FC = () => {
// 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);
@@ -100,15 +101,19 @@ export const VaultDialog: React.FC = () => {
| undefined
>(undefined);
+ // Form ref
+ const formRef = useRef(
+ createFormRef({
+ password: "",
+ confirmPassword: "",
+ newPassword: ""
+ })
+ );
+
// Input refs
const passInputRef = useRef(null);
const confirmPassRef = useRef(null);
- const changePassInputRef = useRef(null);
-
- // Password refs
- const passwordRef = useRef(null);
- const confirmPasswordRef = useRef(null);
- const newPasswordRef = useRef(null);
+ const newPassInputRef = useRef(null);
const close = useCallback(() => {
if (loading) {
@@ -123,10 +128,11 @@ export const VaultDialog: React.FC = () => {
Navigation.queueRoutesForUpdate();
- // Reset password refs
- passwordRef.current = null;
- confirmPasswordRef.current = null;
- newPasswordRef.current = null;
+ // Reset form values and errors
+ formRef.current.setValue("password", "");
+ formRef.current.setValue("confirmPassword", "");
+ formRef.current.setValue("newPassword", "");
+ formRef.current.clearErrors();
// Reset refs
requestTypeRef.current = null;
@@ -144,8 +150,6 @@ export const VaultDialog: React.FC = () => {
// Reset UI state
setVisible(false);
setLoading(false);
- setWrongPassword(false);
- setPasswordsDontMatch(false);
setDeleteAll(false);
setBiometricUnlock(false);
setIsBiometryAvailable(false);
@@ -155,9 +159,10 @@ export const VaultDialog: React.FC = () => {
const deleteVault = useCallback(async () => {
setLoading(true);
try {
+ const { password } = formRef.current.getValues();
let verified = true;
if (await db.user.getUser()) {
- verified = await db.user.verifyPassword(passwordRef.current || "");
+ verified = await db.user.verifyPassword(password);
}
if (verified) {
let noteIds: string[] = [];
@@ -195,11 +200,7 @@ export const VaultDialog: React.FC = () => {
}, 100);
} else {
setLoading(false);
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
+ formRef.current.setError("password", strings.passwordIncorrect());
}
} catch (e) {
console.error(e);
@@ -209,11 +210,12 @@ export const VaultDialog: React.FC = () => {
const clearVault = useCallback(async () => {
setLoading(true);
try {
+ const { password } = formRef.current.getValues();
const vault = await db.vaults.default();
const relations = await db.relations.from(vault!, "note").get();
const noteIds = relations.map((item) => item.toId);
- await db.vault.clear(passwordRef.current || "");
+ await db.vault.clear(password);
noteIds.forEach((id) => {
eSendEvent(
@@ -233,11 +235,7 @@ export const VaultDialog: React.FC = () => {
type: "success"
});
} catch (e) {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
+ formRef.current.setError("password", strings.passwordIncorrect());
}
setLoading(false);
}, [close]);
@@ -270,24 +268,14 @@ export const VaultDialog: React.FC = () => {
);
const takeErrorAction = useCallback(() => {
- setWrongPassword(true);
+ formRef.current.setError("password", strings.passwordIncorrect());
setVisible(true);
- setTimeout(() => {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
- }, 500);
}, []);
const lockNote = useCallback(async () => {
- if (!passwordRef.current || passwordRef.current.trim() === "") {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
+ const { password } = formRef.current.getValues();
+ if (!password || password.trim() === "") {
+ formRef.current.setError("password", strings.passwordIncorrect());
return;
} else {
await db.vault.add(noteRef.current!.id);
@@ -305,8 +293,9 @@ export const VaultDialog: React.FC = () => {
}, [close]);
const permanantUnlock = useCallback(() => {
+ const { password } = formRef.current.getValues();
db.vault
- .remove(noteRef.current!.id, passwordRef.current || "")
+ .remove(noteRef.current!.id, password)
.then(async () => {
ToastManager.show({
heading: strings.noteUnlocked(),
@@ -315,7 +304,7 @@ export const VaultDialog: React.FC = () => {
});
eSendEvent(eUpdateNoteInEditor, noteRef.current, true);
if (biometricUnlock && !isBiometryEnrolled) {
- await enrollFingerprint(passwordRef.current || "");
+ await enrollFingerprint(password);
}
close();
})
@@ -375,8 +364,9 @@ export const VaultDialog: React.FC = () => {
);
const deleteNote = useCallback(async () => {
+ const { password } = formRef.current.getValues();
try {
- await db.vault.remove(noteRef.current!.id, passwordRef.current || "");
+ await db.vault.remove(noteRef.current!.id, password);
await deleteItems("note", [noteRef.current!.id]);
close();
} catch (e) {
@@ -385,16 +375,14 @@ export const VaultDialog: React.FC = () => {
}, [close, takeErrorAction]);
const openNote = useCallback(async () => {
+ const { password } = formRef.current.getValues();
try {
- if (!passwordRef.current) throw new Error("Invalid password");
+ if (!password) throw new Error("Invalid password");
- const note = await db.vault.open(
- noteRef.current!.id,
- passwordRef.current
- );
+ const note = await db.vault.open(noteRef.current!.id, password);
if (!note) throw new Error("Failed to unlock note.");
if (biometricUnlock && !isBiometryEnrolled) {
- await enrollFingerprint(passwordRef.current || "");
+ await enrollFingerprint(password);
}
const requestType = requestTypeRef.current;
@@ -411,7 +399,6 @@ export const VaultDialog: React.FC = () => {
requestType === VaultRequestType.CustomAction &&
onUnlockRef.current
) {
- const password = passwordRef.current;
const unlock = onUnlockRef.current;
close();
await sleep(500);
@@ -433,12 +420,9 @@ export const VaultDialog: React.FC = () => {
]);
const unlockNote = useCallback(async () => {
- if (!passwordRef.current || passwordRef.current.trim() === "") {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
+ const { password } = formRef.current.getValues();
+ if (!password || password.trim() === "") {
+ formRef.current.setError("password", strings.passwordIncorrect());
return;
}
if (requestTypeRef.current === VaultRequestType.PermanentUnlock) {
@@ -449,10 +433,11 @@ export const VaultDialog: React.FC = () => {
}, [permanantUnlock, openNote]);
const createVault = useCallback(async () => {
- await db.vault.create(passwordRef.current || "");
+ const { password } = formRef.current.getValues();
+ await db.vault.create(password);
if (biometricUnlock) {
- await enrollFingerprint(passwordRef.current || "");
+ await enrollFingerprint(password);
}
if (noteRef.current?.id) {
await db.vault.add(noteRef.current.id);
@@ -504,36 +489,21 @@ export const VaultDialog: React.FC = () => {
if (loading) return;
- if (!passwordRef.current) {
- ToastManager.show({
- heading: strings.passwordNotEntered(),
- type: "error",
- context: "local"
- });
- return;
- }
+ if (!formRef.current.validate()) return;
+
+ const { password, newPassword } = formRef.current.getValues();
if (requestType === VaultRequestType.CreateVault) {
- if (passwordRef.current !== confirmPasswordRef.current) {
- ToastManager.show({
- heading: strings.passwordNotMatched(),
- type: "error",
- context: "local"
- });
- setPasswordsDontMatch(true);
- return;
- }
-
createVault();
} else if (requestType === VaultRequestType.ChangePassword) {
setLoading(true);
db.vault
- .changePassword(passwordRef.current, newPasswordRef.current || "")
+ .changePassword(password, newPassword)
.then(() => {
setLoading(false);
if (biometricUnlock) {
- enrollFingerprint(newPasswordRef.current || "");
+ enrollFingerprint(newPassword);
}
ToastManager.show({
heading: strings.passwordUpdated(),
@@ -545,37 +515,23 @@ export const VaultDialog: React.FC = () => {
.catch((e) => {
setLoading(false);
if (e.message === VAULT_ERRORS.wrongPassword) {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
+ formRef.current.setError("password", strings.passwordIncorrect());
} else {
- ToastManager.error(e);
+ console.error(e);
}
});
} else if (requestType === VaultRequestType.LockNote) {
- if (!passwordRef.current || passwordRef.current.trim() === "") {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error",
- context: "local"
- });
- setWrongPassword(true);
- return;
- }
db.vault
- .unlock(passwordRef.current)
+ .unlock(password)
.then(async (unlocked) => {
if (unlocked) {
- setWrongPassword(false);
await lockNote();
} else {
- takeErrorAction();
+ formRef.current.setError("password", strings.passwordIncorrect());
}
})
.catch((e) => {
- takeErrorAction();
+ formRef.current.setError("password", strings.passwordIncorrect());
});
} else if (
requestType === VaultRequestType.UnlockNote ||
@@ -586,22 +542,13 @@ export const VaultDialog: React.FC = () => {
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 {
console.log("Error: Note should be locked for this operation");
}
} else if (requestType === VaultRequestType.EnableFingerprint) {
- enrollFingerprint(passwordRef.current);
+ enrollFingerprint(password);
} else if (requestType === VaultRequestType.ClearVault) {
await clearVault();
} else if (requestType === VaultRequestType.DeleteVault) {
@@ -616,7 +563,6 @@ export const VaultDialog: React.FC = () => {
enrollFingerprint,
unlockNote,
lockNote,
- takeErrorAction,
clearVault,
deleteVault
]);
@@ -632,7 +578,7 @@ export const VaultDialog: React.FC = () => {
if (!credentials) throw new Error("Failed to get user credentials");
if (credentials?.password) {
- passwordRef.current = credentials.password;
+ formRef.current.setValue("password", credentials.password);
onPress();
} else {
eSendEvent(eCloseActionSheet);
@@ -679,8 +625,6 @@ export const VaultDialog: React.FC = () => {
setIsBiometryAvailable(available);
setIsBiometryEnrolled(fingerprint);
setBiometricUnlock(fingerprint);
- setWrongPassword(false);
- setPasswordsDontMatch(false);
setDeleteAll(false);
setLoading(false);
@@ -772,14 +716,14 @@ export const VaultDialog: React.FC = () => {
isCustomAction) &&
!isRevokeFingerprint ? (
<>
- {
- passwordRef.current = value;
- }}
+ autoComplete="password"
marginBottom={
!biometricUnlock ||
!isBiometryEnrolled ||
@@ -789,14 +733,13 @@ export const VaultDialog: React.FC = () => {
? 0
: 10
}
- onSubmit={() => {
+ onSubmitEditing={() => {
if (isChangePassword) {
- confirmPassRef.current?.focus();
+ newPassInputRef.current?.focus();
} else {
onPress();
}
}}
- autoComplete="password"
returnKeyLabel={
isChangePassword ? strings.next() : titleRef.current
}
@@ -807,6 +750,7 @@ export const VaultDialog: React.FC = () => {
? strings.currentPassword()
: strings.password()
}
+ validators={[validators.required(strings.passwordRequired())]}
/>
{!biometricUnlock ||
@@ -849,70 +793,68 @@ export const VaultDialog: React.FC = () => {
{isChangePassword ? (
<>
- {
- newPasswordRef.current = value;
- }}
autoComplete="password"
- onSubmit={() => {
+ onSubmitEditing={() => {
onPress();
}}
returnKeyLabel="Change"
returnKeyType="done"
secureTextEntry
placeholder={strings.newPassword()}
+ validators={[validators.required(strings.passwordRequired())]}
/>
>
) : null}
{isCreateVault ? (
- {
- passwordRef.current = value;
- }}
autoComplete="password"
returnKeyLabel={strings.next()}
returnKeyType="next"
secureTextEntry
- onSubmit={() => {
+ onSubmitEditing={() => {
confirmPassRef.current?.focus();
}}
placeholder={strings.password()}
+ validators={[validators.required(strings.passwordRequired())]}
/>
- passwordRef.current || ""}
- errorMessage="Passwords do not match."
- onErrorCheck={() => null}
- marginBottom={0}
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
- onChangeText={(value) => {
- confirmPasswordRef.current = value;
- if (value !== passwordRef.current) {
- setPasswordsDontMatch(true);
- } else {
- setPasswordsDontMatch(false);
- }
- }}
- onSubmit={() => {
+ marginBottom={0}
+ onSubmitEditing={() => {
onPress();
}}
placeholder={strings.confirmPassword()}
+ validators={[
+ validators.required(strings.confirmPasswordRequired()),
+ validators.matchField(
+ "password",
+ strings.passwordNotMatched()
+ )
+ ]}
/>
) : null}
From 8c8f085b16b98395b5381dda4baa3124f80e6127 Mon Sep 17 00:00:00 2001
From: kashaf-ansari-dev
Date: Thu, 4 Jun 2026 06:51:23 +0500
Subject: [PATCH 2/8] mobile: remove unused validationType from FormInput
Signed-off-by: kashaf-ansari-dev
---
apps/mobile/app/components/dialogs/vault/index.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/apps/mobile/app/components/dialogs/vault/index.tsx b/apps/mobile/app/components/dialogs/vault/index.tsx
index 20edd9f1c..97bba6df8 100644
--- a/apps/mobile/app/components/dialogs/vault/index.tsx
+++ b/apps/mobile/app/components/dialogs/vault/index.tsx
@@ -839,7 +839,6 @@ export const VaultDialog: React.FC = () => {
autoCapitalize="none"
testID={notesnook.ids.dialogs.vault.pwdAlt}
secureTextEntry
- validationType="confirmPassword"
autoComplete="password"
returnKeyLabel="Create"
returnKeyType="done"
From a6a74b924ee8b407c2ce1751f4d6a352fe6fcc62 Mon Sep 17 00:00:00 2001
From: kashaf-ansari-dev
Date: Tue, 9 Jun 2026 10:03:37 +0500
Subject: [PATCH 3/8] mobile: show inline password error on note unlock instead
of toast
Signed-off-by: kashaf-ansari-dev
---
apps/mobile/app/screens/editor/index.tsx | 48 +++++++++++--------
.../editor/tiptap/use-editor-events.tsx | 5 +-
.../editor-mobile/src/components/editor.tsx | 44 ++++++++++++++---
3 files changed, 70 insertions(+), 27 deletions(-)
diff --git a/apps/mobile/app/screens/editor/index.tsx b/apps/mobile/app/screens/editor/index.tsx
index 896399e31..b3e95a27d 100755
--- a/apps/mobile/app/screens/editor/index.tsx
+++ b/apps/mobile/app/screens/editor/index.tsx
@@ -64,6 +64,7 @@ import { strings } from "@notesnook/intl";
import { i18n } from "@lingui/core";
import { useVaultStatus } from "../../hooks/use-vault-status";
import { useSettingStore } from "../../stores/use-setting-store";
+import { NativeEvents } from "@notesnook/editor-mobile/src/utils/native-events";
const style: ViewStyle = {
height: "100%",
@@ -268,17 +269,22 @@ const useLockedNoteHandler = () => {
const onSubmit = async ({
password,
- biometrics: enrollBiometrics
+ biometrics: enrollBiometrics,
+ resolverId
}: {
password: string;
biometrics?: boolean;
+ resolverId?: string;
}) => {
if (!tabRef.current?.session?.noteId || !tabRef.current) return;
+
if (!password || password.trim().length === 0) {
- ToastManager.show({
- heading: strings.passwordNotEntered(),
- type: "error"
- });
+ if (resolverId) {
+ editorController.current?.postMessage(NativeEvents.resolve, {
+ resolverId,
+ data: { success: false, error: strings.passwordNotEntered() }
+ });
+ }
return;
}
@@ -287,6 +293,7 @@ const useLockedNoteHandler = () => {
tabRef.current?.session?.noteId,
password
);
+
if (enrollBiometrics && note) {
try {
const unlocked = await db.vault.unlock(password);
@@ -298,7 +305,6 @@ const useLockedNoteHandler = () => {
type: "success",
context: "global"
});
-
const biometry = await BiometricService.isBiometryAvailable();
const fingerprint = await BiometricService.hasInternetCredentials();
useTabStore.setState({
@@ -306,22 +312,24 @@ const useLockedNoteHandler = () => {
biometryEnrolled: !!fingerprint
});
syncTabs();
- } catch (e) {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error"
- });
- }
+ } catch (e) {}
}
- eSendEvent(eOnLoadNote, {
- item: note,
- refresh: true
- });
+
+ if (resolverId) {
+ editorController.current?.postMessage(NativeEvents.resolve, {
+ resolverId,
+ data: { success: true }
+ });
+ }
+
+ eSendEvent(eOnLoadNote, { item: note, refresh: true });
} catch (e) {
- ToastManager.show({
- heading: strings.passwordIncorrect(),
- type: "error"
- });
+ if (resolverId) {
+ editorController.current?.postMessage(NativeEvents.resolve, {
+ resolverId,
+ data: { success: false, error: strings.passwordIncorrect() }
+ });
+ }
}
};
diff --git a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx
index 55d37e9fa..330ff3991 100644
--- a/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx
+++ b/apps/mobile/app/screens/editor/tiptap/use-editor-events.tsx
@@ -721,7 +721,10 @@ export const useEditorEvents = (
}
case EditorEvents.unlock: {
- eSendEvent(eUnlockWithPassword, editorMessage.value);
+ eSendEvent(eUnlockWithPassword, {
+ ...editorMessage.value,
+ resolverId: editorMessage.resolverId
+ });
break;
}
diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx
index 697893636..2bcb73829 100644
--- a/packages/editor-mobile/src/components/editor.tsx
+++ b/packages/editor-mobile/src/components/editor.tsx
@@ -65,6 +65,7 @@ const Tiptap = ({
const tab = useTabContext();
const isFocused = useTabStore((state) => state.currentTab === tab?.id);
const [tick, setTick] = useState(0);
+ const [passwordError, setPasswordError] = useState(null);
const containerRef = useRef(null);
const noteStateUpdateTimer = useRef();
const tabRef = useRef(tab);
@@ -660,15 +661,31 @@ const Tiptap = ({