From 3960853e61509602fd3418323b676d082d3e1bf9 Mon Sep 17 00:00:00 2001 From: 2ManyProjects Date: Wed, 30 Apr 2025 23:37:19 -0500 Subject: [PATCH] Cookie Consent --- frontend/src/App.jsx | 8 +- .../src/components/CookieConsentPopup.jsx | 193 ++++++++++++++++++ .../src/components/CookieSettingsButton.jsx | 166 +++++++++++++++ frontend/src/components/Footer.jsx | 16 +- frontend/src/services/consentService.js | 107 ++++++++++ 5 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/CookieConsentPopup.jsx create mode 100644 frontend/src/components/CookieSettingsButton.jsx create mode 100644 frontend/src/services/consentService.js diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 96dbdb4..d2ad55f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import { StripeProvider } from './context/StripeContext'; import useBrandingSettings from '@hooks/brandingHooks'; import imageUtils from '@utils/imageUtils'; import Clarity from '@microsoft/clarity'; +import CookieConsentPopup from '@components/CookieConsentPopup'; // Import layouts import MainLayout from './layouts/MainLayout'; @@ -50,7 +51,6 @@ const BrandingPage = lazy(() => import('@pages/Admin/BrandingPage')); const projectId = "rcjhrd0t72" -Clarity.init(projectId); // Loading component for suspense fallback @@ -64,6 +64,11 @@ function App() { // Use the centralized hook to fetch branding settings const { data: brandingSettings, isLoading } = useBrandingSettings(); + useEffect(() => { + Clarity.init(projectId); + Clarity.consent({ clarity: false }); + }, []); + // Update the document head with branding settings useEffect(() => { if (brandingSettings) { @@ -114,6 +119,7 @@ function App() { }> + {/* Main routes with MainLayout */} }> diff --git a/frontend/src/components/CookieConsentPopup.jsx b/frontend/src/components/CookieConsentPopup.jsx new file mode 100644 index 0000000..49b4c6f --- /dev/null +++ b/frontend/src/components/CookieConsentPopup.jsx @@ -0,0 +1,193 @@ +import React, { useState, useEffect } from 'react'; +import { + Card, + CardContent, + Typography, + Button, + Box, + IconButton, + Radio, + RadioGroup, + FormControlLabel, + FormControl, + FormLabel, + Slide, + Divider +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import CookieIcon from '@mui/icons-material/Cookie'; +import consentService, { CONSENT_LEVELS } from '../services/consentService'; + +/** + * Cookie consent popup component + * Shows a popup for cookie consent on first visit and manages Clarity tracking + */ +const CookieConsentPopup = () => { + const [open, setOpen] = useState(false); + const [consentLevel, setConsentLevel] = useState(CONSENT_LEVELS.NECESSARY); + + useEffect(() => { + // Check if user has already set cookie preferences + const storedConsent = consentService.getStoredConsent(); + + if (!storedConsent) { + // First visit, show the popup + setOpen(true); + } else { + // Apply stored consent settings + setConsentLevel(storedConsent.level); + consentService.applyConsentSettings(storedConsent.level); + } + }, []); + + // Handle consent level change + const handleConsentLevelChange = (event) => { + setConsentLevel(event.target.value); + }; + + // Save user preferences and close popup + const handleSave = () => { + consentService.saveConsent(consentLevel); + setOpen(false); + }; + + // Accept all cookies + const handleAcceptAll = () => { + const allConsent = CONSENT_LEVELS.ALL; + setConsentLevel(allConsent); + consentService.saveConsent(allConsent); + setOpen(false); + }; + + // Accept only necessary cookies + const handleNecessaryOnly = () => { + const necessaryOnly = CONSENT_LEVELS.NECESSARY; + setConsentLevel(necessaryOnly); + consentService.saveConsent(necessaryOnly); + setOpen(false); + }; + + // Reopen the consent popup (for testing or if user wants to change settings) + // This function can be exposed via a preferences button somewhere in your app + const reopenConsentPopup = () => { + setOpen(true); + }; + + if (!open) return null; + + return ( + + + + + + + Cookie Settings + + setOpen(false)}> + + + + + + We use cookies to enhance your browsing experience, provide personalized content, + and analyze our traffic. Please choose your privacy settings below. + + + + Privacy Settings + + } + label={ + + Necessary Cookies Only - Essential for website functionality + + } + /> + } + label={ + + Functional Cookies - For enhanced functionality and preferences + + } + /> + } + label={ + + Analytics Cookies - To understand how you use our website + + } + /> + } + label={ + + All Cookies - Accept all cookies for the best experience + + } + /> + + + + + + + + + + + + + + + + ); +}; + +// Expose reopen function for others to use +export const reopenCookieConsent = () => { + const consentElement = document.getElementById('cookie-consent-reopen'); + if (consentElement) { + consentElement.click(); + } +}; + +export default CookieConsentPopup; \ No newline at end of file diff --git a/frontend/src/components/CookieSettingsButton.jsx b/frontend/src/components/CookieSettingsButton.jsx new file mode 100644 index 0000000..e95004f --- /dev/null +++ b/frontend/src/components/CookieSettingsButton.jsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { Button, IconButton, Tooltip, Dialog, DialogTitle, DialogContent, + DialogActions, Typography, Box, Divider, RadioGroup, Radio, FormControlLabel, + FormControl, FormLabel } from '@mui/material'; +import CookieIcon from '@mui/icons-material/Cookie'; +import consentService, { CONSENT_LEVELS } from '../services/consentService'; + +/** + * Button to open cookie settings dialog + * Can be placed in the footer or other accessible areas of the app + */ +const CookieSettingsButton = ({ variant = 'text', icon = false, label = 'Cookie Settings' }) => { + const [open, setOpen] = useState(false); + const [consentLevel, setConsentLevel] = useState(() => { + const storedConsent = consentService.getStoredConsent(); + return storedConsent?.level || CONSENT_LEVELS.NECESSARY; + }); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const handleConsentLevelChange = (event) => { + setConsentLevel(event.target.value); + }; + + const handleSave = () => { + consentService.saveConsent(consentLevel); + setOpen(false); + }; + + const handleAcceptAll = () => { + const allConsent = CONSENT_LEVELS.ALL; + setConsentLevel(allConsent); + consentService.saveConsent(allConsent); + setOpen(false); + }; + + const handleNecessaryOnly = () => { + const necessaryOnly = CONSENT_LEVELS.NECESSARY; + setConsentLevel(necessaryOnly); + consentService.saveConsent(necessaryOnly); + setOpen(false); + }; + + // Render as icon button or regular button + if (icon) { + return ( + <> + + + + + + {renderDialog()} + + ); + } + + return ( + <> + + {renderDialog()} + + ); + + function renderDialog() { + return ( + + + + + Cookie Settings + + + + + We use cookies to enhance your browsing experience, provide personalized content, + and analyze our traffic. You can adjust your privacy settings below. + + + + + + Privacy Settings + + } + label={ + + Necessary Cookies Only + + Essential cookies required for basic website functionality. These cannot be disabled. + + + } + /> + } + label={ + + Functional Cookies + + Cookies that remember your preferences and enhance website functionality. + + + } + /> + } + label={ + + Analytics Cookies + + Cookies that help us understand how you use our website and improve your experience. + This includes Microsoft Clarity for analyzing site usage patterns. + + + } + /> + } + label={ + + All Cookies + + Accept all cookies including analytics, marketing, and third-party cookies. + + + } + /> + + + + + + + You can change these settings at any time by clicking on the Cookie Settings link in the footer. + + + + + + + + + + + ); + } +}; + +export default CookieSettingsButton; \ No newline at end of file diff --git a/frontend/src/components/Footer.jsx b/frontend/src/components/Footer.jsx index d1a7ffa..94009c8 100644 --- a/frontend/src/components/Footer.jsx +++ b/frontend/src/components/Footer.jsx @@ -1,10 +1,11 @@ import React from 'react'; -import { Box, Container, Grid, Typography, Link, IconButton } from '@mui/material'; +import { Box, Container, Grid, Typography, Link, IconButton, Divider } from '@mui/material'; import FacebookIcon from '@mui/icons-material/Facebook'; import TwitterIcon from '@mui/icons-material/Twitter'; import InstagramIcon from '@mui/icons-material/Instagram'; import { Link as RouterLink } from 'react-router-dom'; import imageUtils from '@utils/imageUtils'; +import CookieSettingsButton from './CookieSettingsButton'; const Footer = ({brandingSettings}) => { @@ -89,6 +90,19 @@ const Footer = ({brandingSettings}) => { + + + + {/* + Privacy Policy + + + Terms of Service + */} + + + + {copyrightText} diff --git a/frontend/src/services/consentService.js b/frontend/src/services/consentService.js new file mode 100644 index 0000000..abed8e8 --- /dev/null +++ b/frontend/src/services/consentService.js @@ -0,0 +1,107 @@ +/** + * Service for managing cookie and tracking consent preferences + */ + +import Clarity from '@microsoft/clarity'; + +// Cookie consent levels +export const CONSENT_LEVELS = { + NECESSARY: 'necessary', + FUNCTIONAL: 'functional', + ANALYTICS: 'analytics', + ALL: 'all' +}; + +// Local storage key for tracking consent +export const CONSENT_STORAGE_KEY = 'rocks_bones_sticks_cookie_consent'; + +/** + * Get stored consent level + * @returns {Object|null} Stored consent data or null if not set + */ +export const getStoredConsent = () => { + const storedConsent = localStorage.getItem(CONSENT_STORAGE_KEY); + return storedConsent ? JSON.parse(storedConsent) : null; +}; + +/** + * Check if user has given analytics consent + * @returns {boolean} Whether analytics is allowed + */ +export const hasAnalyticsConsent = () => { + const consent = getStoredConsent(); + if (!consent) return false; + + return consent.level === CONSENT_LEVELS.ANALYTICS || + consent.level === CONSENT_LEVELS.ALL; +}; + +/** + * Check if user has given functional cookies consent + * @returns {boolean} Whether functional cookies are allowed + */ +export const hasFunctionalConsent = () => { + const consent = getStoredConsent(); + if (!consent) return false; + + return consent.level === CONSENT_LEVELS.FUNCTIONAL || + consent.level === CONSENT_LEVELS.ANALYTICS || + consent.level === CONSENT_LEVELS.ALL; +}; + +/** + * Apply consent settings to tracking tools + * @param {string} level - Consent level to apply + */ +export const applyConsentSettings = (level) => { + // Determine if analytics is allowed based on consent level + const allowAnalytics = level === CONSENT_LEVELS.ANALYTICS || level === CONSENT_LEVELS.ALL; + + if (allowAnalytics) { + // Enable Clarity tracking + Clarity.consent(); + } else { + // Disable Clarity tracking + Clarity.consent({ clarity: false }); + } + + console.log(`Applied consent level: ${level}, analytics ${allowAnalytics ? 'enabled' : 'disabled'}`); +}; + +/** + * Save user consent preferences + * @param {string} level - Consent level to save + */ +export const saveConsent = (level) => { + // Save consent to local storage + localStorage.setItem( + CONSENT_STORAGE_KEY, + JSON.stringify({ + level, + timestamp: new Date().toISOString() + }) + ); + + // Apply the consent settings + applyConsentSettings(level); +}; + +/** + * Clear stored consent preferences + */ +export const clearConsent = () => { + localStorage.removeItem(CONSENT_STORAGE_KEY); + + // Reset to most restrictive level + applyConsentSettings(CONSENT_LEVELS.NECESSARY); +}; + +export default { + CONSENT_LEVELS, + getStoredConsent, + hasAnalyticsConsent, + hasFunctionalConsent, + applyConsentSettings, + saveConsent, + clearConsent +}; \ No newline at end of file