1import { useMemo, useState } from "react";
2
3import { cn } from "../../lib/utils";
4import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
5
6const REDACTED_TEXT_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
7
8export function redactedPlaceholder(value: string): string {
9 let state = 0x811c9dc5;
10 for (let index = 0; index < value.length; index += 1) {
11 state ^= value.charCodeAt(index);
12 state = Math.imul(state, 0x01000193);
13 }
14
15 const nextChar = () => {
16 state = Math.imul(state ^ (state >>> 13), 0x85ebca6b);
17 state = Math.imul(state ^ (state >>> 16), 0xc2b2ae35);
18 return REDACTED_TEXT_ALPHABET[Math.abs(state) % REDACTED_TEXT_ALPHABET.length] ?? "x";
19 };
20
21 return Array.from(value, (char) => {
22 if (char === "@" || char === "." || char === "-" || char === "_") return char;
23 return nextChar();
24 }).join("");
25}
26
27export function RedactedSensitiveText(props: {
28 readonly value: string | null | undefined;
29 readonly ariaLabel: string;
30 readonly revealTooltip: string;
31 readonly hideTooltip: string;
32 readonly className?: string;
33}) {
34 const [revealed, setRevealed] = useState(false);
35 const value = props.value?.trim();
36 const redacted = useMemo(() => (value ? redactedPlaceholder(value) : ""), [value]);
37
38 if (!value) return null;
39
40 return (
41 <Tooltip>
42 <TooltipTrigger
43 render={
44 <button
45 type="button"
46 className={cn(
47 "min-w-0 cursor-pointer rounded-sm font-mono text-2xs leading-normal transition hover:text-foreground",
48 revealed ? "text-muted-foreground" : "select-none text-muted-foreground blur-xs",
49 props.className,
50 )}
51 onClick={() => setRevealed((current) => !current)}
52 aria-label={props.ariaLabel}
53 >
54 {revealed ? value : redacted}
55 </button>
56 }
57 />
58 <TooltipPopup side="top">{revealed ? props.hideTooltip : props.revealTooltip}</TooltipPopup>
59 </Tooltip>
60 );
61}