Guia de implementação do consentimento de cookies e do SPA

Compreender o desafio

As Aplicações de Página Única (SPAs) criadas com React, Vue, Angular ou outras estruturas apresentam desafios únicos para o Consentimento de Cookies do TermsFeed porque:

  1. Não há atualização da página. A página não é recarregada como as páginas web clássicas.
  2. Conteúdo dinâmico. O conteúdo carrega dinamicamente sem acionar novamente o DOMContentLoaded.
  3. Mudanças de rota. A navegação ocorre via JavaScript, e não através do carregamento tradicional de páginas.
  4. Gestão do estado. O estado de consentimento deve persistir ao longo das alterações de rota.

Solução: Recarregar na navegação

Problema

«Tenho uma SPA React e o banner de cookies só aparece no primeiro carregamento da página. Quando os utilizadores navegam para outras páginas, o banner não volta a aparecer se não tiverem feito uma escolha. Além disso, se aceitarem cookies numa página, os scripts não carregam nas páginas seguintes até que haja uma atualização completa.»

Solução

Ao implementar o Consentimento de Cookies do TermsFeed num SPA, é necessário:

  1. Inicializar o consentimento no primeiro carregamento.
  2. Detectar alterações de rota.
  3. Recarregar o banner, se necessário.
  4. Verificar novamente o estado do consentimento.

Implementação no React

Passo 1: Criar um componente de consentimento de cookies

// CookieConsent.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const CookieConsent = () => {
    const location = useLocation();

    useEffect(() => {
        // Load TermsFeed script if not already loaded
        if (!window.cookieconsent) {
            const script = document.createElement('script');
            script.src = '//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js';
            script.charset = 'UTF-8';
            script.onload = () => initializeCookieConsent();
            document.head.appendChild(script);
        } else {
            // Script already loaded, just check if banner needs to be shown
            checkBannerState();
        }
    }, []); // Run once on mount

    useEffect(() => {
        // Check banner state on route change
        if (window.cookieconsent) {
            checkBannerState();
        }
    }, [location]); // Run on every route change

    const initializeCookieConsent = () => {
        if (window.cookieconsent && typeof window.cookieconsent.run === 'function') {
            window.cookieconsent.run({
                "notice_banner_type": "headline",
                "consent_type": "express",
                "palette": "light",
                "language": "en",
                "website_name": "My SPA",
                "website_privacy_policy_url": "https://mysite.com/privacy",
                "open_preferences_center_selector": "#changePreferences",
                "page_load_consent_levels": ["strictly-necessary"],
                "callbacks": {
                    "scripts_specific_loaded": (level) => {
                        console.log("Scripts loaded for: " + level);
                        // Re-initialize any components that need tracking
                        if (level === 'tracking') {
                            reinitializeAnalytics();
                        }
                    }
                }
            });
        }
    };

    const checkBannerState = () => {
        // Check if user has already made a consent choice
        const consentCookie = getCookie('cookie_consent_level');
        if (!consentCookie) {
            // No consent yet, banner should be visible
            // No action needed - banner will show automatically
        }
    };

    const getCookie = (name) => {
        const value = ; ${document.cookie};
        const parts = value.split(; ${name}=);
        if (parts.length === 2) return parts.pop().split(';').shift();
    };

    const reinitializeAnalytics = () => {
        // Re-run any analytics initialization needed
        if (window.gtag) {
            window.gtag('config', 'G-XXXXXXXXXX', {
                'page_path': location.pathname
            });
        }
    };

    return null; // This component doesn't render anything
};

export default CookieConsent;

Passo 2: Adicionar ao componente da aplicação

// App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import CookieConsent from './components/CookieConsent';
import Home from './pages/Home';
import About from './pages/About';

function App() {
    return (
        <Router>
            <CookieConsent />
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/about" element={<About />} />
            </Routes>

            {/ Preferences button /}
            <button id="changePreferences" style={{
                position: 'fixed',
                bottom: '20px',
                right: '20px',
                zIndex: 9999
            }}>
                Cookie Settings
            </button>

            <noscript>
                Free cookie consent management tool by <a href="https://www.termsfeed.com/">TermsFeed</a>
            </noscript>
        </Router>
    );
}

export default App;

Implementação Vue.js

Passo 1: Criar o plugin Cookie Consent

// plugins/cookieConsent.js
export default {
    install(app, options) {
        // Load script
        const loadCookieConsent = () => {
            if (window.cookieconsent) return Promise.resolve();

            return new Promise((resolve, reject) => {
                const script = document.createElement('script');
                script.src = '//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js';
                script.charset = 'UTF-8';
                script.onload = () => resolve();
                script.onerror = () => reject();
                document.head.appendChild(script);
            });
        };

        // Initialize
        const initCookieConsent = () => {
            if (window.cookieconsent && typeof window.cookieconsent.run === 'function') {
                window.cookieconsent.run({
                    "notice_banner_type": "headline",
                    "consent_type": "express",
                    "palette": "light",
                    "language": "en",
                    "website_name": options.websiteName || "My Vue SPA",
                    "website_privacy_policy_url": options.privacyUrl || "#",
                    "open_preferences_center_selector": "#changePreferences",
                    "page_load_consent_levels": ["strictly-necessary"]
                });
            }
        };

        // Make available globally
        app.config.globalProperties.$cookieConsent = {
            load: loadCookieConsent,
            init: initCookieConsent
        };

        // Auto-initialize on mount
        if (typeof window !== 'undefined') {
            loadCookieConsent().then(() => {
                document.addEventListener('DOMContentLoaded', initCookieConsent);
            });
        }
    }
};

Passo 2: Utilizar na aplicação principal

// main.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import cookieConsentPlugin from './plugins/cookieConsent';

const router = createRouter({
    history: createWebHistory(),
    routes: [
        { path: '/', component: () => import('./views/Home.vue') },
        { path: '/about', component: () => import('./views/About.vue') }
    ]
});

const app = createApp(App);

app.use(router);
app.use(cookieConsentPlugin, {
    websiteName: 'My Vue App',
    privacyUrl: 'https://mysite.com/privacy'
});

app.mount('#app');

Passo 3: Criar o componente App

<!-- App.vue -->
<template>
    <div id="app">
        <router-view />

        <button id="changePreferences" class="cookie-settings-btn">
            Cookie Settings
        </button>

        <noscript>
            Free cookie consent management tool by
            <a href="https://www.termsfeed.com/">TermsFeed</a>
        </noscript>
    </div>
</template>

<script>
export default {
    name: 'App',
    mounted() {
        // Watch for route changes
        this.$router.afterEach((to, from) => {
            // Check if consent banner needs to be re-shown
            this.checkConsentState();
        });
    },
    methods: {
        checkConsentState() {
            const consentCookie = document.cookie
                .split('; ')
                .find(row => row.startsWith('cookie_consent_level='));

            if (!consentCookie) {
                console.log('No consent given yet');
            }
        }
    }
};
</script>

<style>
.cookie-settings-btn {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 9999;
    padding: 10px 20px;
    background: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}
</style>

Implementação em Angular

Passo 1: Criar o serviço de consentimento de cookies

// services/cookie-consent.service.ts
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

declare global {
    interface Window {
        cookieconsent: any;
    }
}

@Injectable({
    providedIn: 'root'
})
export class CookieConsentService {
    private scriptLoaded = false;

    constructor(private router: Router) {}

    initialize(): Promise<void> {
        return this.loadScript().then(() => {
            this.initializeConsent();
            this.watchRouteChanges();
        });
    }

    private loadScript(): Promise<void> {
        if (this.scriptLoaded || window.cookieconsent) {
            return Promise.resolve();
        }

        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = '//www.termsfeed.com/public/cookie-consent/4.2.0/cookie-consent.js';
            script.charset = 'UTF-8';
            script.onload = () => {
                this.scriptLoaded = true;
                resolve();
            };
            script.onerror = () => reject();
            document.head.appendChild(script);
        });
    }

    private initializeConsent(): void {
        if (window.cookieconsent && typeof window.cookieconsent.run === 'function') {
            window.cookieconsent.run({
                notice_banner_type: 'headline',
                consent_type: 'express',
                palette: 'light',
                language: 'en',
                website_name: 'My Angular App',
                website_privacy_policy_url: 'https://mysite.com/privacy',
                open_preferences_center_selector: '#changePreferences',
                page_load_consent_levels: ['strictly-necessary'],
                callbacks: {
                    scripts_specific_loaded: (level: string) => {
                        console.log('Scripts loaded for:', level);
                        this.handleConsentUpdate(level);
                    }
                }
            });
        }
    }

    private watchRouteChanges(): void {
        this.router.events.pipe(
            filter(event => event instanceof NavigationEnd)
        ).subscribe(() => {
            // Check consent state on route change
            this.checkConsentState();
        });
    }

    private checkConsentState(): void {
        const consentCookie = this.getCookie('cookie_consent_level');
        if (!consentCookie) {
            console.log('No consent given yet');
        }
    }

    private handleConsentUpdate(level: string): void {
        // Handle consent updates
        if (level === 'tracking') {
            this.initializeAnalytics();
        }
    }

    private initializeAnalytics(): void {
        // Re-initialize analytics on consent
        if ((window as any).gtag) {
            (window as any).gtag('config', 'G-XXXXXXXXXX', {
                page_path: this.router.url
            });
        }
    }

    private getCookie(name: string): string | null {
        const value = ; ${document.cookie};
        const parts = value.split(; ${name}=);
        if (parts.length === 2) {
            return parts.pop()?.split(';').shift() || null;
        }
        return null;
    }

    openPreferences(): void {
        if (window.cookieconsent && typeof window.cookieconsent.openPreferencesCenter === 'function') {
            window.cookieconsent.openPreferencesCenter();
        }
    }
}

Passo 2: Inicializar no componente da aplicação

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { CookieConsentService } from './services/cookie-consent.service';

@Component({
    selector: 'app-root',
    template: 
        <router-outlet></router-outlet>

        <button id="changePreferences" class="cookie-settings-btn">
            Cookie Settings
        </button>

        <noscript>
            Free cookie consent management tool by
            <a href="https://www.termsfeed.com/">TermsFeed</a>
        </noscript>
    ,
    styles: [
        .cookie-settings-btn {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
    ]
})
export class AppComponent implements OnInit {
    constructor(private cookieConsentService: CookieConsentService) {}

    ngOnInit(): void {
        this.cookieConsentService.initialize().catch(error => {
            console.error('Failed to initialize cookie consent:', error);
        });
    }
}

Problemas comuns com SPA e consentimento de cookies

Problema 1: O banner não reaparece durante a navegação

Problema

O utilizador navega sem fazer uma escolha e o banner desaparece.

Solução

Verifique se o consentimento foi dado na mudança de rota.

// Check if consent was given on route change
router.afterEach(() => {
    const hasConsent = getCookie('cookie_consent_level');
    if (!hasConsent) {
        // Banner should be visible - it will show automatically
        // No manual action needed if properly configured
    }
});

Problema 2: Os scripts não carregam após o consentimento em novas rotas

Problema

«O utilizador aceita os cookies, navega para outra página na minha aplicação React, mas o Google Analytics não regista a visualização da nova página.»

Solução

Use callbacks para rastrear a visualização da página em cada mudança de rota.

// In your callback
"callbacks": {
    "scripts_specific_loaded": (level) => {
        if (level === 'tracking' && window.gtag) {
            // Track page view on every route change
            router.afterEach((to) => {
                gtag('config', 'G-XXXXXXXXXX', {
                    'page_path': to.path
                });
            });
        }
    }
}

Problema 3: Aparecem várias instâncias do banner

Problema

O banner aparece várias vezes ao navegar rapidamente.

Solução

Utilize um guard para impedir inicializações múltiplas

// Use a guard to prevent multiple initializations
let isInitialized = false;

const initCookieConsent = () => {
    if (isInitialized) return;
    isInitialized = true;

    cookieconsent.run({ / config / });
};

Abrir o Centro de Preferências programaticamente

Para SPAs, poderá querer abrir as preferências sem um botão estático:

// Method 1: Using the openPreferencesCenter() method
const openPreferences = () => {
    if (window.cookieconsent && typeof window.cookieconsent.openPreferencesCenter === 'function') {
        window.cookieconsent.openPreferencesCenter();
    } else {
        console.error('Cookie consent not initialized');
    }
};

// In React
<button onClick={openPreferences}>Change Cookie Settings</button>

// In Vue
<button @click="openPreferences">Change Cookie Settings</button>

// In Angular
<button (click)="openPreferences()">Change Cookie Settings</button>