> ## Documentation Index
> Fetch the complete documentation index at: https://neezon.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Contact Neezon — Shopify app support and partnerships

> Reach Neezon for support, partnerships, and general questions about our Shopify apps.

export const ContactForm = () => {
  const WORKER_ENDPOINT = 'https://neezon.com/contact/action';
  const TURNSTILE_SITE_KEY = '0x4AAAAAABSViXJPqmc17S2l';
  const TURNSTILE_SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
  const inputClass = 'mt-2 w-full rounded-xl border border-zinc-200 bg-zinc-50/50 px-4 py-3 text-sm text-zinc-900 outline-none transition focus:border-indigo-500 focus:ring-2 focus:ring-purple-500 dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100';
  const labelClass = 'block text-sm font-medium text-zinc-800 dark:text-zinc-200';
  const [mounted, setMounted] = useState(false);
  const formRef = useRef(null);
  const widgetContainerRef = useRef(null);
  const widgetIdRef = useRef(null);
  const scriptPromiseRef = useRef(null);
  const [token, setToken] = useState('');
  const [status, setStatus] = useState('idle');
  const [errorMessage, setErrorMessage] = useState('');
  useEffect(() => {
    setMounted(true);
  }, []);
  const loadTurnstileScript = () => {
    if (typeof window === 'undefined') return Promise.resolve();
    if (window.turnstile) return Promise.resolve();
    if (scriptPromiseRef.current) return scriptPromiseRef.current;
    scriptPromiseRef.current = new Promise((resolve, reject) => {
      const existing = document.querySelector('script[data-neezon-turnstile="1"]');
      if (existing) {
        if (window.turnstile) return resolve();
        existing.addEventListener('load', () => resolve());
        existing.addEventListener('error', () => reject(new Error('Turnstile script failed to load.')));
        return;
      }
      const script = document.createElement('script');
      script.src = TURNSTILE_SCRIPT_SRC;
      script.async = true;
      script.defer = true;
      script.dataset.neezonTurnstile = '1';
      script.onload = () => resolve();
      script.onerror = () => reject(new Error('Turnstile script failed to load.'));
      document.head.appendChild(script);
    });
    return scriptPromiseRef.current;
  };
  useEffect(() => {
    if (!mounted) return;
    let cancelled = false;
    loadTurnstileScript().then(() => {
      if (cancelled) return;
      if (!widgetContainerRef.current || !window.turnstile) return;
      if (widgetIdRef.current != null) return;
      widgetIdRef.current = window.turnstile.render(widgetContainerRef.current, {
        sitekey: TURNSTILE_SITE_KEY,
        theme: 'auto',
        callback: freshToken => setToken(freshToken),
        'error-callback': () => setToken(''),
        'expired-callback': () => setToken(''),
        'timeout-callback': () => setToken('')
      });
    }).catch(() => {
      if (!cancelled) {
        setErrorMessage('Captcha failed to load. Please refresh the page.');
      }
    });
    return () => {
      cancelled = true;
      if (widgetIdRef.current != null && typeof window !== 'undefined' && window.turnstile) {
        try {
          window.turnstile.remove(widgetIdRef.current);
        } catch (_) {}
      }
      widgetIdRef.current = null;
    };
  }, [mounted]);
  const resetCaptcha = () => {
    if (widgetIdRef.current != null && typeof window !== 'undefined' && window.turnstile) {
      try {
        window.turnstile.reset(widgetIdRef.current);
      } catch (_) {}
    }
    setToken('');
  };
  const handleSubmit = async event => {
    event.preventDefault();
    if (status === 'loading') return;
    setErrorMessage('');
    if (!token) {
      setStatus('error');
      setErrorMessage('Please complete the captcha before sending.');
      return;
    }
    const formEl = event.currentTarget;
    const formData = new FormData(formEl);
    const payload = {
      name: String(formData.get('name') || '').trim(),
      email: String(formData.get('email') || '').trim(),
      message: String(formData.get('message') || '').trim(),
      turnstileToken: token
    };
    if (!payload.name || !payload.email || !payload.message) {
      setStatus('error');
      setErrorMessage('Please fill in your name, email, and message.');
      return;
    }
    setStatus('loading');
    try {
      const response = await fetch(WORKER_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });
      let data = null;
      try {
        data = await response.json();
      } catch (_) {
        data = null;
      }
      if (!response.ok || !data?.ok) {
        const serverMessage = data && typeof data.error === 'string' ? data.error : null;
        throw new Error(serverMessage || `Request failed with status ${response.status}.`);
      }
      setStatus('success');
      if (formRef.current) formRef.current.reset();
      resetCaptcha();
    } catch (err) {
      setStatus('error');
      setErrorMessage(err && err.message || 'Something went wrong while sending your message. Please try again.');
      resetCaptcha();
    }
  };
  if (!mounted) {
    return <div className="mt-8 min-h-[520px]" aria-hidden="true" />;
  }
  const isSubmitting = status === 'loading';
  const isDisabled = isSubmitting || !token;
  return <form ref={formRef} onSubmit={handleSubmit} className="mt-8 space-y-5" noValidate suppressHydrationWarning>
      <div>
        <label htmlFor="contact-name" className={labelClass}>
          Name
        </label>
        <input id="contact-name" name="name" type="text" required autoComplete="name" disabled={isSubmitting} className={inputClass} />
      </div>

      <div>
        <label htmlFor="contact-email" className={labelClass}>
          Email
        </label>
        <input id="contact-email" name="email" type="email" required autoComplete="email" disabled={isSubmitting} className={inputClass} />
      </div>

      <div>
        <label htmlFor="contact-message" className={labelClass}>
          Message
        </label>
        <textarea id="contact-message" name="message" required rows={6} disabled={isSubmitting} className={`${inputClass} block min-h-40 resize-y`} />
      </div>

      {}
      <div ref={widgetContainerRef} suppressHydrationWarning />

      {status === 'error' && errorMessage && <p role="alert" className="rounded-xl border border-rose-200 bg-rose-50/80 px-4 py-3 text-sm text-rose-700 dark:border-rose-900/60 dark:bg-rose-950/40 dark:text-rose-300">
          {errorMessage}
        </p>}

      {status === 'success' && <p role="status" className="rounded-xl border border-emerald-200 bg-emerald-50/80 px-4 py-3 text-sm text-emerald-800 dark:border-emerald-900/60 dark:bg-emerald-950/40 dark:text-emerald-300">
          Thanks — your message is on its way. We’ll get back to you at the email you provided.
        </p>}

      <button type="submit" disabled={isDisabled} aria-busy={isSubmitting} className="flex w-full items-center justify-center rounded-xl bg-indigo-600 py-3.5 text-sm font-semibold text-white transition hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-indigo-500 dark:hover:bg-indigo-400">
        {isSubmitting ? 'Sending…' : 'Send message'}
      </button>
    </form>;
};

<div className="flex min-h-screen flex-col bg-zinc-50 dark:bg-zinc-950">
  <div className="flex-1">
    <div className="border-b border-zinc-200/80 bg-white dark:border-zinc-800 dark:bg-zinc-900/30">
      <div className="mx-auto max-w-6xl px-6 py-16 md:py-20">
        <h1 className="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-50 md:text-4xl">
          Let’s talk
        </h1>

        <p className="mt-4 max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
          Apps, billing, partnerships, or anything about Neezon—send a note and we will get back to you.
        </p>
      </div>
    </div>

    <div className="mx-auto max-w-6xl px-6 py-14 md:py-16">
      <div className="grid gap-10 lg:grid-cols-12 lg:gap-14">
        <div className="lg:col-span-5">
          <div className="sticky top-8 space-y-8">
            <div className="rounded-3xl border border-zinc-200/90 bg-white p-8 shadow-sm dark:border-zinc-800 dark:bg-zinc-900/50">
              <h2 className="text-xs font-semibold uppercase tracking-wider text-indigo-800 dark:text-purple-400">Email</h2>

              <p className="mt-4 text-zinc-700 dark:text-zinc-300">
                <a href="mailto:support@neezon.com" className="text-lg font-semibold text-indigo-700 hover:underline dark:text-purple-400">
                  [support@neezon.com](mailto:support@neezon.com)
                </a>
              </p>

              <p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">We aim to reply within one business day.</p>
            </div>

            <div className="rounded-3xl border border-dashed border-zinc-300 bg-white/60 p-8 dark:border-zinc-700 dark:bg-zinc-900/30">
              <h2 className="text-xs font-semibold uppercase tracking-wider text-zinc-500 dark:text-zinc-500">Shopify merchants</h2>

              <p className="mt-3 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
                For in-app issues, use the support options inside Shopify Admin after installing our apps—often the fastest path with your store context.
              </p>
            </div>
          </div>
        </div>

        <div className="lg:col-span-7">
          <div className="rounded-3xl border border-zinc-200/90 bg-white p-8 shadow-lg shadow-zinc-900/5 dark:border-zinc-800 dark:bg-zinc-900/60 md:p-10">
            <h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50">Send a message</h2>

            <p className="mt-2 text-sm text-zinc-600 dark:text-zinc-400">
              Include your store URL and, if relevant, which Neezon app you use. We reply at the email address you provide.
            </p>

            <ContactForm />
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
