277 lines
9.7 KiB
TypeScript
277 lines
9.7 KiB
TypeScript
import { TargetedEvent, TargetedKeyboardEvent } from "preact";
|
|
import { useEffect, useRef } from "preact/hooks";
|
|
import { EncryptionModalProps } from "./welcome-types.ts";
|
|
|
|
/**
|
|
* EncryptionModal Component
|
|
* Modal banner for setting up encryption password or changing PIN
|
|
* Appears at top of screen until password is set or skipped
|
|
*
|
|
* @param showModal - Whether modal is visible
|
|
* @param tempPassword - Current password input value
|
|
* @param passwordError - Error message to display
|
|
* @param isChangeMode - Whether modal is in change PIN mode
|
|
* @param onPasswordChange - Handler for password input changes
|
|
* @param onSetPassword - Handler to validate and set password
|
|
* @param onSkip - Handler to skip encryption setup
|
|
* @param onClose - Handler to close change PIN modal
|
|
*
|
|
* TODO: Future enhancements
|
|
* - Add password strength indicator
|
|
* - Add password visibility toggle
|
|
* - Add "Remember this device" checkbox
|
|
* - Add password requirements tooltip
|
|
* - Add biometric authentication option
|
|
* - Add password confirmation field
|
|
*/
|
|
export default function EncryptionModal({
|
|
showModal,
|
|
tempPassword,
|
|
passwordError,
|
|
isChangeMode = false,
|
|
showRemovePinConfirm = false,
|
|
persistAcrossDevices,
|
|
onPasswordChange,
|
|
onSetPassword,
|
|
onSkip,
|
|
onClose,
|
|
onRemovePinConfirm,
|
|
onPersistAcrossDevicesChange,
|
|
}: EncryptionModalProps) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (showModal && inputRef.current) {
|
|
inputRef.current.focus();
|
|
}
|
|
}, [showModal]);
|
|
|
|
if (!showModal) return null;
|
|
|
|
const handleCheckboxChange = (e: TargetedEvent<HTMLInputElement>) => {
|
|
onPersistAcrossDevicesChange(e.currentTarget.checked);
|
|
};
|
|
|
|
const handlePasswordInput = (
|
|
e: TargetedEvent<HTMLInputElement, InputEvent>,
|
|
) => {
|
|
onPasswordChange(e.currentTarget.value);
|
|
};
|
|
|
|
const handleKeyDown = (e: TargetedKeyboardEvent<HTMLInputElement>) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
if (tempPassword.length >= 4) {
|
|
onSetPassword();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (e: TargetedEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
if (tempPassword.length >= 4) {
|
|
onSetPassword();
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
if (isChangeMode) {
|
|
onClose?.();
|
|
} else {
|
|
// Clear the input when skipping initial encryption setup
|
|
onPasswordChange("");
|
|
onSkip();
|
|
}
|
|
};
|
|
|
|
const handleBackdropClick = (
|
|
e: TargetedEvent<HTMLDivElement, MouseEvent>,
|
|
) => {
|
|
if (e.currentTarget === e.target) {
|
|
if (isChangeMode) {
|
|
onClose?.();
|
|
} else {
|
|
onSkip();
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleInputBlur = () => {
|
|
if (tempPassword.length === 0 && !isChangeMode) {
|
|
onSkip();
|
|
}
|
|
};
|
|
|
|
const isPasswordValid = tempPassword.length >= 4;
|
|
|
|
return (
|
|
<>
|
|
{/* Main Change PIN Modal */}
|
|
<div
|
|
class="fixed inset-0 z-50 flex items-start justify-center pt-2 p-4 bg-transparent"
|
|
role="dialog"
|
|
aria-labelledby="encryption-modal-title"
|
|
onClick={handleBackdropClick}
|
|
>
|
|
<div class="bg-white/65 rounded-2xl shadow-2xl p-6 w-full max-w-md mx-auto transform transition-all duration-200 scale-100">
|
|
<div class="flex flex-col bg-opacity-10 items-center text-center space-y-4">
|
|
<div class="flex-1 bg-opacity-10 min-w-0">
|
|
<h3
|
|
id="encryption-modal-title"
|
|
class="text-lg font-semibold text-gray-800"
|
|
>
|
|
{isChangeMode ? "Change PIN" : "Enter PIN"}
|
|
</h3>
|
|
<p class="text-sm text-gray-700 mt-1">
|
|
{isChangeMode
|
|
? "Enter your new PIN to update your encryption"
|
|
: "Enter your PIN to access your encrypted data"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 space-y-4">
|
|
<form onSubmit={handleSubmit} class="flex flex-col space-y-2">
|
|
<input
|
|
ref={inputRef}
|
|
id="encryption-password"
|
|
name="encryption-password"
|
|
type="password"
|
|
value={tempPassword}
|
|
onInput={handlePasswordInput}
|
|
onKeyDown={handleKeyDown}
|
|
onBlur={handleInputBlur}
|
|
placeholder={isChangeMode
|
|
? "Enter new PIN (min 4 digits)"
|
|
: "Enter PIN (min 4 digits)"}
|
|
autocomplete="current-password"
|
|
aria-invalid={!!passwordError}
|
|
aria-describedby={passwordError
|
|
? "password-error"
|
|
: "password-hint"}
|
|
class={`w-full px-4 py-3 border rounded-xl shadow-sm focus:outline-none focus:ring-2 focus:border-blue-500 text-base text-gray-800 placeholder-gray-600 text-center tracking-widest bg-white ${
|
|
passwordError
|
|
? "border-red-300 focus:ring-red-500"
|
|
: "border-gray-300 focus:ring-blue-500"
|
|
}`}
|
|
/>
|
|
{passwordError && (
|
|
<div
|
|
id="password-error"
|
|
class="text-red-600 text-base font-medium"
|
|
role="alert"
|
|
>
|
|
{passwordError}
|
|
</div>
|
|
)}
|
|
{tempPassword.length > 0 && tempPassword.length < 4 && (
|
|
<p id="password-hint" class="text-sm text-red-700">
|
|
PIN must be at least 4 characters
|
|
</p>
|
|
)}
|
|
{isPasswordValid && (
|
|
<p id="password-hint" class="text-sm text-green-700">
|
|
✓ PIN valid - press Enter or click{" "}
|
|
{isChangeMode ? "Update" : "Continue"}
|
|
</p>
|
|
)}
|
|
|
|
{/* Persist across devices checkbox - only show in initial setup, not change mode */}
|
|
{!isChangeMode && (
|
|
<div class="flex items-center space-x-2 mt-3">
|
|
<input
|
|
id="persist-across-devices"
|
|
type="checkbox"
|
|
checked={persistAcrossDevices}
|
|
onChange={handleCheckboxChange}
|
|
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 focus:ring-2"
|
|
/>
|
|
<label
|
|
htmlFor="persist-across-devices"
|
|
class="text-sm text-gray-700 cursor-pointer select-none"
|
|
>
|
|
Persist across devices
|
|
</label>
|
|
</div>
|
|
)}
|
|
</form>
|
|
|
|
<div class="flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onSetPassword}
|
|
disabled={!isPasswordValid && !isChangeMode}
|
|
aria-label={isChangeMode
|
|
? "Update encryption PIN"
|
|
: "Set encryption PIN"}
|
|
class={`flex-1 px-4 py-3 rounded-xl text-base font-medium transition-colors duration-200 ${
|
|
(isPasswordValid ||
|
|
(isChangeMode && tempPassword.length === 0))
|
|
? "bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 hover:bg-gray-100"
|
|
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
|
}`}
|
|
>
|
|
{isChangeMode ? "Update" : "Continue"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleCancel}
|
|
class="px-6 py-3 text-base bg-gray-200 text-gray-500 hover:text-gray-700 font-medium transition-colors duration-200 rounded-xl hover:bg-gray-100"
|
|
aria-label={isChangeMode
|
|
? "Cancel PIN change"
|
|
: "Skip encryption setup"}
|
|
>
|
|
{isChangeMode ? "Cancel" : "Skip"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confirmation Dialog for PIN Removal */}
|
|
{showRemovePinConfirm && onRemovePinConfirm && onSkip && (
|
|
<div
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
|
|
role="dialog"
|
|
aria-labelledby="remove-pin-confirm-title"
|
|
>
|
|
<div class="bg-white rounded-2xl shadow-2xl p-6 w-full max-w-sm mx-auto transform transition-all duration-200 scale-100">
|
|
<div class="flex flex-col items-center text-center space-y-4">
|
|
<div class="flex-1 min-w-0">
|
|
<h3
|
|
id="remove-pin-confirm-title"
|
|
class="text-lg font-semibold text-gray-800"
|
|
>
|
|
Remove PIN?
|
|
</h3>
|
|
<p class="text-sm text-gray-700 mt-2">
|
|
This will remove the previously saved PIN
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 flex gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={onRemovePinConfirm}
|
|
class="flex-1 px-4 py-3 bg-red-600 text-white rounded-xl text-base font-medium transition-colors duration-200 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
aria-label="Remove PIN"
|
|
>
|
|
OK
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onSkip}
|
|
class="flex-1 px-4 py-3 bg-gray-200 text-gray-700 rounded-xl text-base font-medium transition-colors duration-200 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
aria-label="Cancel PIN removal"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|