diff --git a/apps/mobile/app/components/dialogs/vault/index.tsx b/apps/mobile/app/components/dialogs/vault/index.tsx index c95885e99..57266e2d3 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[] = []; @@ -175,6 +180,7 @@ export const VaultDialog: React.FC = () => { noteIds = relations.map((item) => item.toId); } await db.vault.delete(deleteAll); + await BiometricService.resetCredentials(); if (deleteAll) { noteIds.forEach((id) => { @@ -195,11 +201,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 +211,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 +236,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]); @@ -257,12 +256,7 @@ export const VaultDialog: React.FC = () => { }); close(); } catch (e) { - close(); - ToastManager.show({ - heading: strings.passwordIncorrect(), - type: "error", - context: "local" - }); + formRef.current.setError("password", strings.passwordIncorrect()); setLoading(false); } }, @@ -270,24 +264,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 +289,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 +300,7 @@ export const VaultDialog: React.FC = () => { }); eSendEvent(eUpdateNoteInEditor, noteRef.current, true); if (biometricUnlock && !isBiometryEnrolled) { - await enrollFingerprint(passwordRef.current || ""); + await enrollFingerprint(password); } close(); }) @@ -375,8 +360,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 +371,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 +395,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 +416,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 +429,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 +485,23 @@ 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 || "") - .then(() => { + .changePassword(password, newPassword) + .then(async () => { setLoading(false); if (biometricUnlock) { - enrollFingerprint(newPasswordRef.current || ""); + enrollFingerprint(newPassword); + } else { + await BiometricService.resetCredentials(); } ToastManager.show({ heading: strings.passwordUpdated(), @@ -545,37 +513,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 +540,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 +561,6 @@ export const VaultDialog: React.FC = () => { enrollFingerprint, unlockNote, lockNote, - takeErrorAction, clearVault, deleteVault ]); @@ -632,7 +576,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 +623,6 @@ export const VaultDialog: React.FC = () => { setIsBiometryAvailable(available); setIsBiometryEnrolled(fingerprint); setBiometricUnlock(fingerprint); - setWrongPassword(false); - setPasswordsDontMatch(false); setDeleteAll(false); setLoading(false); @@ -772,14 +714,14 @@ export const VaultDialog: React.FC = () => { isCustomAction) && !isRevokeFingerprint ? ( <> - { - passwordRef.current = value; - }} + autoComplete="password" marginBottom={ !biometricUnlock || !isBiometryEnrolled || @@ -789,14 +731,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 +748,7 @@ export const VaultDialog: React.FC = () => { ? strings.currentPassword() : strings.password() } + validators={[validators.required(strings.passwordRequired())]} /> {!biometricUnlock || @@ -849,70 +791,67 @@ 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} 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/core/src/api/vault.ts b/packages/core/src/api/vault.ts index 0489f6e12..1e299e4b7 100644 --- a/packages/core/src/api/vault.ts +++ b/packages/core/src/api/vault.ts @@ -182,7 +182,6 @@ export default class Vault { async remove(noteId: string, password: string) { await this.unlockNote(noteId, password, true); - if (!(await this.exists())) await this.create(password); await this.db.relations.to({ id: noteId, type: "note" }, "vault").unlink(); } @@ -203,9 +202,11 @@ export default class Vault { ); if (password) { - this.password = password; - if (!(await this.exists())) await this.create(password); + try { + await this.unlock(password); + } catch {} } + return { ...note, content }; } diff --git a/packages/editor-mobile/src/components/editor.tsx b/packages/editor-mobile/src/components/editor.tsx index 697893636..4601eba21 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); @@ -84,6 +85,9 @@ const Tiptap = ({ undo, redo }; + useEffect(() => { + setPasswordError(null); + }, [tab.session?.noteId]); logger("info", tabRef.current.id, "rendering"); @@ -660,15 +664,31 @@ const Tiptap = ({

{ + onSubmit={async (e) => { e.preventDefault(); const data = new FormData(e.currentTarget); const password = data.get("password"); + if (!password) { + setPasswordError(strings.enterPassword()); + return; + } + + setPasswordError(null); const biometrics = data.get("enrollBiometrics"); - post("editor-events:unlock", { - password, - biometrics: biometrics === "on" ? true : false - }); + + const result = await postAsyncWithTimeout( + EditorEvents.unlock, + { + password, + biometrics: biometrics === "on" + } + ); + + if (result && !result.success) { + setPasswordError( + result.error || strings.passwordIncorrect() + ); + } }} style={{ display: "flex", @@ -683,7 +703,9 @@ const Tiptap = ({ ref={controller.passwordInputRef} name="password" type="password" - required + onChange={() => { + if (passwordError) setPasswordError(null); + }} style={{ boxSizing: "border-box", width: 300, @@ -698,6 +720,19 @@ const Tiptap = ({ color: colors.primary.paragraph }} /> + {passwordError ? ( +

+ {passwordError} +

+ ) : null}