@ -35,12 +35,14 @@ pub struct ReportModalProps {
/// Modal dialog for reporting a quote.
///
/// Renders a reason textarea, a Cloudflare Turnstile CAPTCHA widget, and
/// Submit / Cancel buttons. The Submit button is disabled until the Turnstile
/// widget has been solved (i.e., the hidden input carries a non-empty token).
/// Submit / Cancel buttons.
///
/// The Turnstile widget is rendered by injecting a `<div class="cf-turnstile">`
/// with the appropriate `data-sitekey` attribute. Turnstile's global JS (loaded
/// in `index.html`) picks up that div automatically.
/// in `index.html`) picks up that div automatically. The token is read from the
/// hidden `cf-turnstile-response` input at submit time and forwarded to the
/// caller; it may be empty if Turnstile has not yet solved (e.g., no network).
/// Rate limiting is enforced at the WAF layer rather than the application layer.
///
/// # Example
///
@ -55,8 +57,6 @@ pub struct ReportModalProps {
#[ function_component(ReportModal) ]
pub fn report_modal ( props : & ReportModalProps ) -> Html {
let reason = use_state ( String ::new ) ;
// Whether the Turnstile CAPTCHA has been solved (token is non-empty).
let captcha_solved = use_state ( | | false ) ;
// --- Handle reason textarea input ---
let on_reason_input = {
@ -71,50 +71,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
} )
} ;
// --- Poll the hidden Turnstile input for a token on the "input" event ---
// Turnstile fires a synthetic "input" event on the hidden field when solved.
// We wire this up via use_effect so we can attach the listener to the DOM.
{
let captcha_solved = captcha_solved . clone ( ) ;
use_effect ( move | | {
let window = web_sys ::window ( ) . expect ( "no global window" ) ;
let document = window . document ( ) . expect ( "no document" ) ;
// Closure to check the hidden input value.
let captcha_solved_clone = captcha_solved . clone ( ) ;
let check_fn = wasm_bindgen ::closure ::Closure ::< dyn Fn ( ) > ::new ( move | | {
let document = web_sys ::window ( )
. and_then ( | w | w . document ( ) )
. expect ( "document" ) ;
if let Some ( input_el ) = document
. query_selector ( "input[name='cf-turnstile-response']" )
. ok ( )
. flatten ( )
{
let input : HtmlInputElement = input_el . unchecked_into ( ) ;
captcha_solved_clone . set ( ! input . value ( ) . is_empty ( ) ) ;
}
} ) ;
// Attach the listener to the document so it catches the event
// regardless of where Turnstile fires it.
let _ = document
. add_event_listener_with_callback ( "input" , check_fn . as_ref ( ) . unchecked_ref ( ) ) ;
// Also run once immediately in case the widget already solved
// (e.g., page revisit).
check_fn
. as_ref ( )
. unchecked_ref ::< js_sys ::Function > ( )
. call0 ( & wasm_bindgen ::JsValue ::NULL )
. ok ( ) ;
// Keep the closure alive until the component unmounts.
check_fn . forget ( ) ;
| | ( )
} ) ;
}
// --- Form submit handler ---
let on_submit = {
let reason = reason . clone ( ) ;
@ -122,7 +78,7 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
Callback ::from ( move | e : SubmitEvent | {
e . prevent_default ( ) ;
// Read the Turnstile token from the hidden input .
// Read the Turnstile token from the hidden input if available .
let token = web_sys ::window ( )
. and_then ( | w | w . document ( ) )
. and_then ( | doc | {
@ -133,10 +89,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
. map ( | el | el . unchecked_into ::< HtmlInputElement > ( ) . value ( ) )
. unwrap_or_default ( ) ;
if token . is_empty ( ) {
return ; // Should not happen — button is disabled until solved.
}
let reason_val = {
let s = ( * reason ) . clone ( ) ;
if s . is_empty ( ) {
@ -151,7 +103,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
} ;
let reason_len = reason . len ( ) ;
let submit_disabled = ! * captcha_solved ;
let on_cancel = props . on_cancel . clone ( ) ;
@ -193,7 +144,6 @@ pub fn report_modal(props: &ReportModalProps) -> Html {
< button
type = "submit"
class = "btn btn--danger"
disabled = { submit_disabled }
>
{ "Submit Report" }
< / button >