feat: add privacy snippet feature in site settings for easy copying to privacy policy
This commit is contained in:
@@ -10,6 +10,7 @@ import VerificationModal from '@/components/sites/VerificationModal'
|
|||||||
import PasswordInput from '@/components/PasswordInput'
|
import PasswordInput from '@/components/PasswordInput'
|
||||||
import Select from '@/components/ui/Select'
|
import Select from '@/components/ui/Select'
|
||||||
import { APP_URL, API_URL } from '@/lib/api/client'
|
import { APP_URL, API_URL } from '@/lib/api/client'
|
||||||
|
import { generatePrivacySnippet } from '@/lib/utils/privacySnippet'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
GearIcon,
|
GearIcon,
|
||||||
@@ -84,6 +85,7 @@ export default function SiteSettingsPage() {
|
|||||||
})
|
})
|
||||||
const [scriptCopied, setScriptCopied] = useState(false)
|
const [scriptCopied, setScriptCopied] = useState(false)
|
||||||
const [linkCopied, setLinkCopied] = useState(false)
|
const [linkCopied, setLinkCopied] = useState(false)
|
||||||
|
const [snippetCopied, setSnippetCopied] = useState(false)
|
||||||
const [showVerificationModal, setShowVerificationModal] = useState(false)
|
const [showVerificationModal, setShowVerificationModal] = useState(false)
|
||||||
const [isPasswordEnabled, setIsPasswordEnabled] = useState(false)
|
const [isPasswordEnabled, setIsPasswordEnabled] = useState(false)
|
||||||
|
|
||||||
@@ -219,6 +221,14 @@ export default function SiteSettingsPage() {
|
|||||||
setTimeout(() => setLinkCopied(false), 2000)
|
setTimeout(() => setLinkCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copySnippet = () => {
|
||||||
|
if (!site) return
|
||||||
|
navigator.clipboard.writeText(generatePrivacySnippet(site))
|
||||||
|
setSnippetCopied(true)
|
||||||
|
toast.success('Privacy snippet copied to clipboard')
|
||||||
|
setTimeout(() => setSnippetCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingOverlay logoSrc="/ciphera_icon_no_margins.png" title="Ciphera Analytics" />
|
return <LoadingOverlay logoSrc="/ciphera_icon_no_margins.png" title="Ciphera Analytics" />
|
||||||
}
|
}
|
||||||
@@ -769,6 +779,45 @@ export default function SiteSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* For your privacy policy */}
|
||||||
|
<div className="space-y-4 pt-6 border-t border-neutral-100 dark:border-neutral-800">
|
||||||
|
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
|
For your privacy policy
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Copy the text below into your site's Privacy Policy to describe your use of Ciphera Analytics.
|
||||||
|
It updates automatically based on your saved settings above.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-500">
|
||||||
|
This is provided for convenience and is not legal advice. You are responsible for ensuring
|
||||||
|
your privacy policy is accurate and complies with applicable laws.
|
||||||
|
</p>
|
||||||
|
<div className="relative">
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
rows={6}
|
||||||
|
value={site ? generatePrivacySnippet(site) : ''}
|
||||||
|
className="w-full px-4 py-3 pr-12 border border-neutral-200 dark:border-neutral-800 rounded-xl
|
||||||
|
bg-neutral-50 dark:bg-neutral-900/50 font-sans text-sm text-neutral-700 dark:text-neutral-300
|
||||||
|
focus:outline-none resize-y"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copySnippet}
|
||||||
|
className="absolute top-3 right-3 p-2 rounded-lg bg-neutral-200 dark:bg-neutral-700
|
||||||
|
hover:bg-neutral-300 dark:hover:bg-neutral-600 text-neutral-600 dark:text-neutral-300
|
||||||
|
transition-colors"
|
||||||
|
title="Copy snippet"
|
||||||
|
>
|
||||||
|
{snippetCopied ? (
|
||||||
|
<CheckIcon className="w-4 h-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="pt-4 border-t border-neutral-100 dark:border-neutral-800 flex justify-end">
|
<div className="pt-4 border-t border-neutral-100 dark:border-neutral-800 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
52
lib/utils/privacySnippet.ts
Normal file
52
lib/utils/privacySnippet.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Site } from '@/lib/api/sites'
|
||||||
|
|
||||||
|
const DOCS_URL =
|
||||||
|
(typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_APP_URL)
|
||||||
|
? `${process.env.NEXT_PUBLIC_APP_URL}/faq`
|
||||||
|
: 'https://analytics.ciphera.net/faq'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a privacy-policy snippet for the site's use of Ciphera Analytics.
|
||||||
|
* The text is derived from the site's data collection and filtering settings
|
||||||
|
* and is intended to be copied into the site owner's Privacy Policy page.
|
||||||
|
* This is for transparency (GDPR Art. 13/14); it is not a cookie banner.
|
||||||
|
*
|
||||||
|
* @param site - The site with current saved settings
|
||||||
|
* @returns Plain-text snippet, two paragraphs
|
||||||
|
*/
|
||||||
|
export function generatePrivacySnippet(site: Site): string {
|
||||||
|
const paths = site.collect_page_paths ?? true
|
||||||
|
const referrers = site.collect_referrers ?? true
|
||||||
|
const device = site.collect_device_info ?? true
|
||||||
|
const geo = site.collect_geo_data || 'full'
|
||||||
|
const screen = site.collect_screen_resolution ?? true
|
||||||
|
const perf = site.enable_performance_insights ?? false
|
||||||
|
const replay = site.replay_mode === 'anonymous_skeleton'
|
||||||
|
const filterBots = site.filter_bots ?? true
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
if (paths) parts.push('which pages are viewed')
|
||||||
|
if (referrers) parts.push('where visitors come from (referrer)')
|
||||||
|
if (device) parts.push('device type, browser, and operating system')
|
||||||
|
if (geo === 'full') parts.push('country, region, and city')
|
||||||
|
else if (geo === 'country') parts.push('country')
|
||||||
|
if (screen) parts.push('screen resolution')
|
||||||
|
if (perf) parts.push('Core Web Vitals (e.g. page load performance)')
|
||||||
|
if (replay) parts.push('anonymised session replays (e.g. clicks and layout; no text you type is stored)')
|
||||||
|
|
||||||
|
const list =
|
||||||
|
parts.length > 0
|
||||||
|
? parts.join(', ')
|
||||||
|
: 'minimal anonymous data about site usage (e.g. that a page was viewed)'
|
||||||
|
|
||||||
|
const p1 =
|
||||||
|
'We use Ciphera Analytics to understand how visitors use our site. Ciphera does not use cookies or other persistent identifiers. A cookie consent banner is not required for Ciphera Analytics. We respect Do Not Track (DNT) browser settings.'
|
||||||
|
|
||||||
|
let p2 = `We collect anonymous data: ${list}. `
|
||||||
|
if (filterBots) {
|
||||||
|
p2 += 'Known bots and referrer spam are excluded from our analytics. '
|
||||||
|
}
|
||||||
|
p2 += `Data is processed in a privacy-preserving way and is not used to identify individuals. For more information, see Ciphera Analytics' documentation: ${DOCS_URL}`
|
||||||
|
|
||||||
|
return `${p1}\n\n${p2}`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user