import { STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_E2E_ENABLED, STRING_ENC_NOT_ENABLED, STRING_LOCAL_ENC_ENABLED, STRING_NON_MATCHING_PASSCODES, StringUtils, Strings, } from '@/Constants/Strings' import { WebApplication } from '@/Application/Application' import { preventRefreshing } from '@/Utils' import { alertDialog } from '@/Services/AlertService' import { ChangeEventHandler, FormEvent, useCallback, useEffect, useRef, useState } from 'react' import { ApplicationEvent } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { ViewControllerManager } from '@/Services/ViewControllerManager' import { Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' import Button from '@/Components/Button/Button' import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' type Props = { application: WebApplication viewControllerManager: ViewControllerManager } const PasscodeLock = ({ application, viewControllerManager }: Props) => { const keyStorageInfo = StringUtils.keyStorageInfo(application) const passcodeAutoLockOptions = application.getAutolockService().getAutoLockIntervalOptions() const { setIsEncryptionEnabled, setIsBackupEncrypted, setEncryptionStatusString } = viewControllerManager.accountMenuController const passcodeInputRef = useRef(null) const [passcode, setPasscode] = useState() const [passcodeConfirmation, setPasscodeConfirmation] = useState() const [selectedAutoLockInterval, setSelectedAutoLockInterval] = useState(null) const [isPasscodeFocused, setIsPasscodeFocused] = useState(false) const [showPasscodeForm, setShowPasscodeForm] = useState(false) const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()) const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()) const handleAddPassCode = () => { setShowPasscodeForm(true) setIsPasscodeFocused(true) } const changePasscodePressed = () => { handleAddPassCode() } const reloadAutoLockInterval = useCallback(async () => { const interval = await application.getAutolockService().getAutoLockInterval() setSelectedAutoLockInterval(interval) }, [application]) const refreshEncryptionStatus = useCallback(() => { const hasUser = application.hasAccount() const hasPasscode = application.hasPasscode() setHasPasscode(hasPasscode) const encryptionEnabled = hasUser || hasPasscode const encryptionStatusString = hasUser ? STRING_E2E_ENABLED : hasPasscode ? STRING_LOCAL_ENC_ENABLED : STRING_ENC_NOT_ENABLED setEncryptionStatusString(encryptionStatusString) setIsEncryptionEnabled(encryptionEnabled) setIsBackupEncrypted(encryptionEnabled) }, [application, setEncryptionStatusString, setIsBackupEncrypted, setIsEncryptionEnabled]) const selectAutoLockInterval = async (interval: number) => { if (!(await application.authorizeAutolockIntervalChange())) { return } await application.getAutolockService().setAutoLockInterval(interval) reloadAutoLockInterval().catch(console.error) } const removePasscodePressed = async () => { await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => { if (await application.removePasscode()) { await application.getAutolockService().deleteAutolockPreference() await reloadAutoLockInterval() refreshEncryptionStatus() } }) } const handlePasscodeChange: ChangeEventHandler = (event) => { const { value } = event.target setPasscode(value) } const handleConfirmPasscodeChange: ChangeEventHandler = (event) => { const { value } = event.target setPasscodeConfirmation(value) } const submitPasscodeForm = async (event: MouseEvent | FormEvent) => { event.preventDefault() if (!passcode || passcode.length === 0) { await alertDialog({ text: Strings.enterPasscode, }) } if (passcode !== passcodeConfirmation) { await alertDialog({ text: STRING_NON_MATCHING_PASSCODES, }) setIsPasscodeFocused(true) return } await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => { const successful = application.hasPasscode() ? await application.changePasscode(passcode as string) : await application.addPasscode(passcode as string) if (!successful) { setIsPasscodeFocused(true) } }) setPasscode(undefined) setPasscodeConfirmation(undefined) setShowPasscodeForm(false) refreshEncryptionStatus() } useEffect(() => { refreshEncryptionStatus() }, [refreshEncryptionStatus]) // `reloadAutoLockInterval` gets interval asynchronously, therefore we call `useEffect` to set initial // value of `selectedAutoLockInterval` useEffect(() => { reloadAutoLockInterval().catch(console.error) }, [reloadAutoLockInterval]) useEffect(() => { if (isPasscodeFocused) { passcodeInputRef.current?.focus() setIsPasscodeFocused(false) } }, [isPasscodeFocused]) // Add the required event observers useEffect(() => { const removeKeyStatusChangedObserver = application.addEventObserver(async () => { setCanAddPasscode(!application.isEphemeralSession()) setHasPasscode(application.hasPasscode()) setShowPasscodeForm(false) }, ApplicationEvent.KeyStatusChanged) return () => { removeKeyStatusChangedObserver() } }, [application]) const cancelPasscodeForm = () => { setShowPasscodeForm(false) setPasscode(undefined) setPasscodeConfirmation(undefined) } return ( <> Passcode Lock {!hasPasscode && canAddPasscode && ( <> Add a passcode to lock the application and encrypt on-device key storage. {keyStorageInfo && {keyStorageInfo}} {!showPasscodeForm &&