Files
zeekayandHanzo Dev 10282c06f6 Rename the persisted keys, and carry returning visitors across
Four localStorage keys still spelled upstream's brand — papermark.email,
papermark.name, last_papermark_login, papermark-all-links-collapsed. The first
two hold a visitor's email and name and are what makes an access form
prefill on a return visit, so renaming them naively signs every returning
visitor out of their own convenience.

lib/webstorage.ts already owned web storage, so the rename lives there:

  - StorageKey names each key once. Eight call sites used to spell the string
    themselves, one of them via a local const, which is why the set had
    drifted into three different naming shapes.
  - LEGACY_KEYS maps each new key to the name it used to have. getItem falls
    back to the old key, copies the value forward and drops the old entry.
    Migrating on read means no boot hook to register and no ordering to get
    wrong; a key migrates the first time something asks for it, and only
    then. setItem and removeItem clear the legacy entry too, so a value
    cannot come back from the dead.

Six of those call sites were reaching for window.localStorage directly and
so skipped the wrapper's try/catch. They now go through it, which is what it
is for — a viewer opening a link in a third-party context with storage
blocked was throwing rather than degrading.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 18:58:44 -07:00

161 lines
4.8 KiB
TypeScript

import {
Dispatch,
SetStateAction,
useEffect,
useRef,
useState,
type CSSProperties,
} from "react";
import { Brand, DataroomBrand } from "@prisma/client";
import { useDebouncedCallback } from "use-debounce";
import { cn } from "@/lib/utils";
import { validateEmail } from "@/lib/utils/validate-email";
import { StorageKey, localStorage } from "@/lib/webstorage";
import { DEFAULT_ACCESS_FORM_TYPE } from ".";
import { useAccessFormTheme } from "./access-form-theme";
export default function EmailSection({
data,
setData,
brand,
disableEditEmail,
useCustomAccessForm,
onValidationChange,
}: {
data: DEFAULT_ACCESS_FORM_TYPE;
setData: Dispatch<SetStateAction<DEFAULT_ACCESS_FORM_TYPE>>;
brand?: Partial<Brand> | Partial<DataroomBrand> | null;
disableEditEmail?: boolean;
useCustomAccessForm?: boolean;
onValidationChange: (isValid: boolean) => void;
}) {
const { email } = data;
const theme = useAccessFormTheme();
const [emailError, setEmailError] = useState<string | null>(null);
const [isDirty, setIsDirty] = useState(false);
useEffect(() => {
// Load email from localStorage when the component mounts
const storedEmail = localStorage.getItem(StorageKey.viewerEmail);
if (storedEmail) {
setData((prevData) => ({
...prevData,
email: storedEmail.toLowerCase(),
}));
}
}, [setData]);
const handleInvalid = (e: React.InvalidEvent<HTMLInputElement>) => {
e.preventDefault(); // Prevent default browser validation popup
setEmailError("Please enter a valid email address");
};
const debouncedValidation = useDebouncedCallback(
(value: string) => {
const isValid = !value || validateEmail(value);
if (isDirty && value && !isValid) {
setEmailError("Please enter a valid email address");
} else {
setEmailError(null);
}
// Notify parent component about validation status
onValidationChange?.(isValid);
},
500, // 500ms delay
);
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newEmail = e.target.value.toLowerCase();
setEmailError(null); // Clear error when typing
debouncedValidation(newEmail);
// Update the state
setData({ ...data, email: newEmail });
// Store in localStorage
localStorage.setItem(StorageKey.viewerEmail, newEmail);
// Optional: Clear error if input becomes valid
if (e.target.validity.valid) {
setEmailError(null);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
setIsDirty(true);
const value = e.target.value;
const isValid = !value || validateEmail(value);
if (value && !isValid) {
setEmailError("Please enter a valid email address");
}
onValidationChange?.(isValid);
};
const handleFocus = () => {
// Optionally clear error when user focuses the input to type again
setEmailError(null);
};
return (
<div className="relative space-y-2">
<label
htmlFor="email"
className="block text-sm font-medium leading-6 text-white"
style={{ color: theme.textColor }}
>
Email address
</label>
<input
name="email"
id="email"
type="email"
autoCorrect="off"
autoComplete="email"
autoFocus
required
translate="no"
className={cn(
"notranslate flex w-full cursor-text rounded-md border-0 bg-black py-1.5 text-gray-500 shadow-sm ring-1 ring-inset ring-gray-600 placeholder:text-[var(--access-placeholder)] focus:ring-2 focus:ring-inset focus:ring-[var(--access-input-focus)] sm:text-sm sm:leading-6",
emailError && isDirty && "ring-red-500",
)}
style={{
backgroundColor: theme.controlBgColor,
borderColor: theme.controlBorderColor,
"--access-placeholder": theme.controlPlaceholderColor,
"--access-input-focus": theme.controlBorderStrongColor,
color: disableEditEmail
? theme.subtleTextColor
: theme.textColor,
} as CSSProperties}
value={email || ""}
placeholder="Enter email"
onChange={handleEmailChange}
onInvalid={handleInvalid}
onBlur={handleBlur}
onFocus={handleFocus}
disabled={disableEditEmail}
data-1p-ignore
aria-invalid={emailError ? "true" : "false"}
aria-describedby={emailError ? "email-error" : undefined}
/>
{emailError && (
<p
id="email-error"
className="mt-1 text-sm text-red-500"
style={{ color: theme.textColor }}
>
{emailError}
</p>
)}
<p className="text-sm" style={{ color: theme.subtleTextColor }}>
{useCustomAccessForm
? "This data will be shared with the content provider."
: "This data will be shared with the sender."}
</p>
</div>
);
}