From aa4ea626188a26b573a3db5424f978ffb5d2a7b7 Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Wed, 12 Aug 2026 18:54:09 -0400 Subject: [PATCH] Validate handle syntax in the atproto login form before submit HandleSearchInput gains an opt-in validateHandle prop: typed values are trimmed, stripped of a leading @, and checked with isValidHandle; invalid ones block submit with a native validation bubble. Suggestion picks skip the check. Enabled on both the log-in and link-account forms. Co-Authored-By: Claude Fable 5 --- components/HandleSearchInput.tsx | 22 +++++++++++++++++++++- components/LoginButton.tsx | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/components/HandleSearchInput.tsx b/components/HandleSearchInput.tsx index d8efa5b9..91f17691 100644 --- a/components/HandleSearchInput.tsx +++ b/components/HandleSearchInput.tsx @@ -1,4 +1,6 @@ "use client"; +import { useRef } from "react"; +import { isValidHandle } from "@atproto/syntax"; import { Input } from "components/Input"; import { Combobox, ComboboxResult } from "components/Combobox"; import { @@ -39,6 +41,9 @@ export const HandleSearchInput = (props: { // handle was entered); cleared on focus via onFocus. highlight?: boolean; onFocus?: () => void; + // Reject typed values that aren't syntactically valid handles with a native + // validation bubble instead of submitting them (suggestion picks are trusted). + validateHandle?: boolean; }) => { let { handleValue, @@ -51,10 +56,23 @@ export const HandleSearchInput = (props: { setHighlighted, } = useActorTypeahead(); + let triggerRef = useRef(null); + const handleSelect = async (handle?: string) => { - const selected = handle ?? handleValue; + let selected = handle ?? handleValue; if (!selected) return; const actor = suggestions.find((s) => s.handle === selected); + if (props.validateHandle && !actor) { + selected = selected.trim().replace(/^@/, ""); + if (!isValidHandle(selected)) { + let input = triggerRef.current?.querySelector("input"); + input?.setCustomValidity( + "Please enter a full handle, like name.bsky.social", + ); + input?.reportValidity(); + return; + } + } setHandleValue(selected); setDropdownOpen(false); setSuggestions([]); @@ -87,6 +105,7 @@ export const HandleSearchInput = (props: { className="w-(--radix-popover-trigger-width)!" trigger={
{ + e.currentTarget.setCustomValidity(""); setHandleValue(e.target.value); props.onChange?.(e.target.value); }} diff --git a/components/LoginButton.tsx b/components/LoginButton.tsx index 29cb3d61..944e6cb6 100644 --- a/components/LoginButton.tsx +++ b/components/LoginButton.tsx @@ -198,6 +198,7 @@ export const LoginContent = (props: { autoFocus={props.open !== false} action={} loading={loading} + validateHandle onSubmit={(handle) => { setLoading(true); window.location.href = buildOauthLoginUrl({ @@ -334,6 +335,7 @@ const LinkAtmosphereContent = (props: { autoFocus={props.open !== false} action={} loading={loading} + validateHandle onSubmit={(handle) => { setLoading(true); window.location.href = buildOauthLoginUrl({ -- 2.54.0