import { WebApplication } from '@/UIModels/Application' import { DialogContent, DialogOverlay } from '@reach/dialog' import { ButtonType, Challenge, ChallengePrompt, ChallengeReason, ChallengeValue, removeFromArray, } from '@standardnotes/snjs' import { ProtectedIllustration } from '@standardnotes/icons' import { FunctionComponent } from 'preact' import { useCallback, useEffect, useState } from 'preact/hooks' import { Button } from '@/Components/Button/Button' import { Icon } from '@/Components/Icon/Icon' import { ChallengeModalPrompt } from './ChallengePrompt' import { LockscreenWorkspaceSwitcher } from './LockscreenWorkspaceSwitcher' import { ApplicationGroup } from '@/UIModels/ApplicationGroup' import { AppState } from '@/UIModels/AppState' type InputValue = { prompt: ChallengePrompt value: string | number | boolean invalid: boolean } export type ChallengeModalValues = Record type Props = { application: WebApplication appState: AppState mainApplicationGroup: ApplicationGroup challenge: Challenge onDismiss?: (challenge: Challenge) => void } const validateValues = (values: ChallengeModalValues, prompts: ChallengePrompt[]): ChallengeModalValues | undefined => { let hasInvalidValues = false const validatedValues = { ...values } for (const prompt of prompts) { const value = validatedValues[prompt.id] if (typeof value.value === 'string' && value.value.length === 0) { validatedValues[prompt.id].invalid = true hasInvalidValues = true } } if (!hasInvalidValues) { return validatedValues } return undefined } export const ChallengeModal: FunctionComponent = ({ application, appState, mainApplicationGroup, challenge, onDismiss, }) => { const [values, setValues] = useState(() => { const values = {} as ChallengeModalValues for (const prompt of challenge.prompts) { values[prompt.id] = { prompt, value: prompt.initialValue ?? '', invalid: false, } } return values }) const [isSubmitting, setIsSubmitting] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [, setProcessingPrompts] = useState([]) const [bypassModalFocusLock, setBypassModalFocusLock] = useState(false) const shouldShowForgotPasscode = [ChallengeReason.ApplicationUnlock, ChallengeReason.Migration].includes( challenge.reason, ) const shouldShowWorkspaceSwitcher = challenge.reason === ChallengeReason.ApplicationUnlock const submit = useCallback(() => { const validatedValues = validateValues(values, challenge.prompts) if (!validatedValues) { return } if (isSubmitting || isProcessing) { return } setIsSubmitting(true) setIsProcessing(true) const valuesToProcess: ChallengeValue[] = [] for (const inputValue of Object.values(validatedValues)) { const rawValue = inputValue.value const value = { prompt: inputValue.prompt, value: rawValue } valuesToProcess.push(value) } const processingPrompts = valuesToProcess.map((v) => v.prompt) setIsProcessing(processingPrompts.length > 0) setProcessingPrompts(processingPrompts) /** * Unfortunately neccessary to wait 50ms so that the above setState call completely * updates the UI to change processing state, before we enter into UI blocking operation * (crypto key generation) */ setTimeout(() => { if (valuesToProcess.length > 0) { application.submitValuesForChallenge(challenge, valuesToProcess).catch(console.error) } else { setIsProcessing(false) } setIsSubmitting(false) }, 50) }, [application, challenge, isProcessing, isSubmitting, values]) const onValueChange = useCallback( (value: string | number, prompt: ChallengePrompt) => { const newValues = { ...values } newValues[prompt.id].invalid = false newValues[prompt.id].value = value setValues(newValues) }, [values], ) const cancelChallenge = useCallback(() => { if (challenge.cancelable) { application.cancelChallenge(challenge) onDismiss?.(challenge) } }, [application, challenge, onDismiss]) useEffect(() => { const removeChallengeObserver = application.addChallengeObserver(challenge, { onValidValue: (value) => { setValues((values) => { const newValues = { ...values } newValues[value.prompt.id].invalid = false return newValues }) setProcessingPrompts((currentlyProcessingPrompts) => { const processingPrompts = currentlyProcessingPrompts.slice() removeFromArray(processingPrompts, value.prompt) setIsProcessing(processingPrompts.length > 0) return processingPrompts }) }, onInvalidValue: (value) => { setValues((values) => { const newValues = { ...values } newValues[value.prompt.id].invalid = true return newValues }) /** If custom validation, treat all values together and not individually */ if (!value.prompt.validates) { setProcessingPrompts([]) setIsProcessing(false) } else { setProcessingPrompts((currentlyProcessingPrompts) => { const processingPrompts = currentlyProcessingPrompts.slice() removeFromArray(processingPrompts, value.prompt) setIsProcessing(processingPrompts.length > 0) return processingPrompts }) } }, onComplete: () => { onDismiss?.(challenge) }, onCancel: () => { onDismiss?.(challenge) }, }) return () => { removeChallengeObserver() } }, [application, challenge, onDismiss]) if (!challenge.prompts) { return null } return ( {challenge.cancelable && ( )}
{challenge.heading}
{challenge.subheading && (
{challenge.subheading}
)}
{ e.preventDefault() submit() }} > {challenge.prompts.map((prompt, index) => ( ))} {shouldShowForgotPasscode && ( )} {shouldShowWorkspaceSwitcher && ( )}
) }