Cookie Consent

This commit is contained in:
2ManyProjects 2025-04-30 23:37:19 -05:00
parent 4b1c0293a0
commit 3960853e61
5 changed files with 488 additions and 2 deletions

View file

@ -7,6 +7,7 @@ import { StripeProvider } from './context/StripeContext';
import useBrandingSettings from '@hooks/brandingHooks'; import useBrandingSettings from '@hooks/brandingHooks';
import imageUtils from '@utils/imageUtils'; import imageUtils from '@utils/imageUtils';
import Clarity from '@microsoft/clarity'; import Clarity from '@microsoft/clarity';
import CookieConsentPopup from '@components/CookieConsentPopup';
// Import layouts // Import layouts
import MainLayout from './layouts/MainLayout'; import MainLayout from './layouts/MainLayout';
@ -50,7 +51,6 @@ const BrandingPage = lazy(() => import('@pages/Admin/BrandingPage'));
const projectId = "rcjhrd0t72" const projectId = "rcjhrd0t72"
Clarity.init(projectId);
// Loading component for suspense fallback // Loading component for suspense fallback
@ -64,6 +64,11 @@ function App() {
// Use the centralized hook to fetch branding settings // Use the centralized hook to fetch branding settings
const { data: brandingSettings, isLoading } = useBrandingSettings(); const { data: brandingSettings, isLoading } = useBrandingSettings();
useEffect(() => {
Clarity.init(projectId);
Clarity.consent({ clarity: false });
}, []);
// Update the document head with branding settings // Update the document head with branding settings
useEffect(() => { useEffect(() => {
if (brandingSettings) { if (brandingSettings) {
@ -114,6 +119,7 @@ function App() {
<StripeProvider> <StripeProvider>
<Suspense fallback={<LoadingComponent />}> <Suspense fallback={<LoadingComponent />}>
<Notifications /> <Notifications />
<CookieConsentPopup />
<Routes> <Routes>
{/* Main routes with MainLayout */} {/* Main routes with MainLayout */}
<Route path="/" element={<MainLayout />}> <Route path="/" element={<MainLayout />}>

View file

@ -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 (
<Slide direction="up" in={open} mountOnEnter unmountOnExit>
<Card
sx={{
position: 'fixed',
bottom: 20,
right: 20,
maxWidth: 400,
boxShadow: 4,
zIndex: 9999
}}
>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CookieIcon color="primary" fontSize="small" sx={{ mr: 1 }} />
<Typography variant="h6">Cookie Settings</Typography>
</Box>
<IconButton size="small" onClick={() => setOpen(false)}>
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<Typography variant="body2" color="text.secondary" paragraph>
We use cookies to enhance your browsing experience, provide personalized content,
and analyze our traffic. Please choose your privacy settings below.
</Typography>
<FormControl component="fieldset" sx={{ my: 2 }}>
<FormLabel component="legend">Privacy Settings</FormLabel>
<RadioGroup
value={consentLevel}
onChange={handleConsentLevelChange}
>
<FormControlLabel
value={CONSENT_LEVELS.NECESSARY}
control={<Radio />}
label={
<Typography variant="body2">
<strong>Necessary Cookies Only</strong> - Essential for website functionality
</Typography>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.FUNCTIONAL}
control={<Radio />}
label={
<Typography variant="body2">
<strong>Functional Cookies</strong> - For enhanced functionality and preferences
</Typography>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.ANALYTICS}
control={<Radio />}
label={
<Typography variant="body2">
<strong>Analytics Cookies</strong> - To understand how you use our website
</Typography>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.ALL}
control={<Radio />}
label={
<Typography variant="body2">
<strong>All Cookies</strong> - Accept all cookies for the best experience
</Typography>
}
/>
</RadioGroup>
</FormControl>
<Divider sx={{ my: 2 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
variant="outlined"
onClick={handleNecessaryOnly}
size="small"
>
Necessary Only
</Button>
<Box>
<Button
variant="outlined"
onClick={handleSave}
sx={{ mr: 1 }}
size="small"
>
Save Preferences
</Button>
<Button
variant="contained"
onClick={handleAcceptAll}
size="small"
>
Accept All
</Button>
</Box>
</Box>
</CardContent>
</Card>
</Slide>
);
};
// Expose reopen function for others to use
export const reopenCookieConsent = () => {
const consentElement = document.getElementById('cookie-consent-reopen');
if (consentElement) {
consentElement.click();
}
};
export default CookieConsentPopup;

View file

@ -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 (
<>
<Tooltip title={label}>
<IconButton onClick={handleOpen} size="small" color="inherit">
<CookieIcon fontSize="small" />
</IconButton>
</Tooltip>
{renderDialog()}
</>
);
}
return (
<>
<Button variant={variant} size="small" onClick={handleOpen}>
{label}
</Button>
{renderDialog()}
</>
);
function renderDialog() {
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CookieIcon sx={{ mr: 1 }} />
Cookie Settings
</Box>
</DialogTitle>
<DialogContent>
<Typography variant="body2" paragraph>
We use cookies to enhance your browsing experience, provide personalized content,
and analyze our traffic. You can adjust your privacy settings below.
</Typography>
<Divider sx={{ my: 2 }} />
<FormControl component="fieldset" sx={{ width: '100%' }}>
<FormLabel component="legend">Privacy Settings</FormLabel>
<RadioGroup
value={consentLevel}
onChange={handleConsentLevelChange}
>
<FormControlLabel
value={CONSENT_LEVELS.NECESSARY}
control={<Radio />}
label={
<Box>
<Typography variant="body1">Necessary Cookies Only</Typography>
<Typography variant="body2" color="text.secondary">
Essential cookies required for basic website functionality. These cannot be disabled.
</Typography>
</Box>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.FUNCTIONAL}
control={<Radio />}
label={
<Box>
<Typography variant="body1">Functional Cookies</Typography>
<Typography variant="body2" color="text.secondary">
Cookies that remember your preferences and enhance website functionality.
</Typography>
</Box>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.ANALYTICS}
control={<Radio />}
label={
<Box>
<Typography variant="body1">Analytics Cookies</Typography>
<Typography variant="body2" color="text.secondary">
Cookies that help us understand how you use our website and improve your experience.
This includes Microsoft Clarity for analyzing site usage patterns.
</Typography>
</Box>
}
/>
<FormControlLabel
value={CONSENT_LEVELS.ALL}
control={<Radio />}
label={
<Box>
<Typography variant="body1">All Cookies</Typography>
<Typography variant="body2" color="text.secondary">
Accept all cookies including analytics, marketing, and third-party cookies.
</Typography>
</Box>
}
/>
</RadioGroup>
</FormControl>
<Divider sx={{ my: 2 }} />
<Typography variant="caption" color="text.secondary">
You can change these settings at any time by clicking on the Cookie Settings link in the footer.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={handleNecessaryOnly} color="inherit">Necessary Only</Button>
<Box sx={{ flexGrow: 1 }} />
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleSave} variant="outlined">Save Preferences</Button>
<Button onClick={handleAcceptAll} variant="contained">Accept All</Button>
</DialogActions>
</Dialog>
);
}
};
export default CookieSettingsButton;

View file

@ -1,10 +1,11 @@
import React from 'react'; 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 FacebookIcon from '@mui/icons-material/Facebook';
import TwitterIcon from '@mui/icons-material/Twitter'; import TwitterIcon from '@mui/icons-material/Twitter';
import InstagramIcon from '@mui/icons-material/Instagram'; import InstagramIcon from '@mui/icons-material/Instagram';
import { Link as RouterLink } from 'react-router-dom'; import { Link as RouterLink } from 'react-router-dom';
import imageUtils from '@utils/imageUtils'; import imageUtils from '@utils/imageUtils';
import CookieSettingsButton from './CookieSettingsButton';
const Footer = ({brandingSettings}) => { const Footer = ({brandingSettings}) => {
@ -89,6 +90,19 @@ const Footer = ({brandingSettings}) => {
</Grid> </Grid>
</Grid> </Grid>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap' }}>
<Box sx={{ display: 'flex', gap: 2 }}>
{/* <Link component={RouterLink} to="/privacy-policy" color="inherit" variant="body2">
Privacy Policy
</Link>
<Link component={RouterLink} to="/terms-of-service" color="inherit" variant="body2">
Terms of Service
</Link> */}
<CookieSettingsButton />
</Box>
</Box>
<Box mt={3}> <Box mt={3}>
<Typography variant="body2" color="text.secondary" align="center"> <Typography variant="body2" color="text.secondary" align="center">
{copyrightText} {copyrightText}

View file

@ -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
};