diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx
index 73b475d..2f104b2 100644
--- a/app/sites/[id]/settings/page.tsx
+++ b/app/sites/[id]/settings/page.tsx
@@ -6,6 +6,7 @@ import { getSite, updateSite, resetSiteData, deleteSite, type Site } from '@/lib
import { getRealtime } from '@/lib/api/stats'
import { toast } from 'sonner'
import LoadingOverlay from '@/components/LoadingOverlay'
+import VerificationModal from '@/components/sites/VerificationModal'
import { APP_URL, API_URL } from '@/lib/api/client'
import { motion, AnimatePresence } from 'framer-motion'
import {
@@ -15,7 +16,7 @@ import {
CheckIcon,
CopyIcon,
ExclamationTriangleIcon,
- LightningBoltIcon
+ LightningBoltIcon,
} from '@radix-ui/react-icons'
const TIMEZONES = [
@@ -54,7 +55,7 @@ export default function SiteSettingsPage() {
})
const [scriptCopied, setScriptCopied] = useState(false)
const [linkCopied, setLinkCopied] = useState(false)
- const [verificationStatus, setVerificationStatus] = useState<'idle' | 'checking' | 'success' | 'error'>('idle')
+ const [showVerificationModal, setShowVerificationModal] = useState(false)
useEffect(() => {
loadSite()
@@ -134,38 +135,6 @@ export default function SiteSettingsPage() {
}
}
- const handleVerify = async () => {
- if (!site?.domain) return
-
- setVerificationStatus('checking')
- let attempts = 0
- const maxAttempts = 10
-
- // 1. Open site
- const protocol = site.domain.includes('http') ? '' : 'https://'
- const verificationUrl = `${protocol}${site.domain}/?utm_source=ciphera_verify&_t=${Date.now()}`
- window.open(verificationUrl, '_blank')
-
- // 2. Poll for success
- const checkInterval = setInterval(async () => {
- attempts++
- try {
- const data = await getRealtime(siteId)
-
- if (data.visitors > 0) {
- clearInterval(checkInterval)
- setVerificationStatus('success')
- toast.success('Connection established!')
- } else if (attempts >= maxAttempts) {
- clearInterval(checkInterval)
- setVerificationStatus('error')
- }
- } catch (e) {
- // Ignore errors while polling
- }
- }, 2000)
- }
-
const copyScript = () => {
const script = ``
navigator.clipboard.writeText(script)
@@ -336,75 +305,21 @@ export default function SiteSettingsPage() {
-
-
-
-
- {/* Status Text */}
- {verificationStatus === 'checking' && (
-
- Waiting for signal from {site.domain}...
-
- )}
-
-
- {/* Error State - Inline Troubleshooting */}
-
- {verificationStatus === 'error' && (
-
-
-
-
-
-
- We couldn't detect the script.
-
-
- Please ensure you opened the new tab and check the following:
-
-
- - Ad blockers are disabled on your site
- - The script is placed in the
<head> tag
- - You are not testing on localhost (unless configured)
-
-
-
-
-
-
- )}
-
+
+
+
+ Check if your site is sending data correctly.
+
+
+
+ setShowVerificationModal(false)}
+ site={site}
+ />
-
)
}
diff --git a/components/sites/VerificationModal.tsx b/components/sites/VerificationModal.tsx
new file mode 100644
index 0000000..806fc93
--- /dev/null
+++ b/components/sites/VerificationModal.tsx
@@ -0,0 +1,230 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { createPortal } from 'react-dom'
+import { motion, AnimatePresence } from 'framer-motion'
+import {
+ Cross2Icon,
+ ExternalLinkIcon,
+ CheckCircledIcon,
+ ExclamationTriangleIcon,
+ LightningBoltIcon
+} from '@radix-ui/react-icons'
+import { Site } from '@/lib/api/sites'
+import { getRealtime } from '@/lib/api/stats'
+import { toast } from 'sonner'
+
+interface VerificationModalProps {
+ isOpen: boolean
+ onClose: () => void
+ site: Site
+}
+
+export default function VerificationModal({ isOpen, onClose, site }: VerificationModalProps) {
+ const [mounted, setMounted] = useState(false)
+ const [status, setStatus] = useState<'idle' | 'checking' | 'success' | 'error'>('idle')
+ const [attempts, setAttempts] = useState(0)
+
+ useEffect(() => {
+ setMounted(true)
+ return () => setMounted(false)
+ }, [])
+
+ useEffect(() => {
+ if (isOpen) {
+ setStatus('idle')
+ setAttempts(0)
+ }
+ }, [isOpen])
+
+ // * Polling Logic
+ useEffect(() => {
+ let interval: NodeJS.Timeout
+ const maxAttempts = 30 // 60 seconds (2s interval)
+
+ if (status === 'checking') {
+ interval = setInterval(async () => {
+ setAttempts(prev => {
+ if (prev >= maxAttempts) {
+ setStatus('error')
+ return prev
+ }
+ return prev + 1
+ })
+
+ try {
+ const data = await getRealtime(site.id)
+ if (data.visitors > 0) {
+ setStatus('success')
+ toast.success('Connection established!')
+ }
+ } catch (e) {
+ // Ignore errors
+ }
+ }, 2000)
+ }
+
+ return () => clearInterval(interval)
+ }, [status, site.id])
+
+ const handleStartVerification = () => {
+ const protocol = site.domain.includes('http') ? '' : 'https://'
+ const verificationUrl = `${protocol}${site.domain}/?utm_source=ciphera_verify&_t=${Date.now()}`
+
+ // * Open site
+ window.open(verificationUrl, '_blank')
+
+ // * Start polling
+ setStatus('checking')
+ setAttempts(0)
+ }
+
+ if (!mounted) return null
+
+ return createPortal(
+
+ {isOpen && (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+
+ {/* Header */}
+
+
+ Verify Installation
+
+
+
+
+ {/* Content */}
+
+ {status === 'idle' && (
+
+
+
+
+
+
+
How this works
+
+ We will open your website in a new tab. Keep it open while we check if the script sends back a signal.
+
+
+
+
+
+
+ )}
+
+ {status === 'checking' && (
+
+
+
+
+ Checking connection...
+
+
+ Waiting for signal from {site.domain}
+
+
+
+ )}
+
+ {status === 'success' && (
+
+
+
+
+
+
+ You're all set!
+
+
+ We are successfully receiving data from your website.
+
+
+
+
+ )}
+
+ {status === 'error' && (
+
+
+
+
+
+
+ Connection Timed Out
+
+
+
+
+
+ Troubleshooting Checklist:
+
+
+ - Did the new tab open successfully?
+ - Is your ad blocker disabled?
+ - Is the script inside the
<head> tag?
+ - Are you running on a valid domain (not localhost)?
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ >
+ )}
+ ,
+ document.body
+ )
+}