Pular para conteúdo

Aplicação SaaS (React)

Visão Geral

Este exemplo demonstra a implementação do Complyr em uma aplicação SaaS moderna com React, incluindo autenticação de usuários, área do cliente e sincronização de preferências de consentimento no backend.

Ideal para dashboards, plataformas web, áreas de membros e aplicações que exigem login de usuários.

Características: - ✅ Identificação de usuários após login - ✅ Sincronização cross-device via email hash - ✅ Preferências persistidas no backend - ✅ Banner condicional (não exibe para usuários com preferências) - ✅ Gestão de cookies na conta do usuário - ✅ Google Tag Manager integrado - ✅ User ID tracking no GA4

Tecnologias: - React 18+ - React Router v6 - localStorage para cache local - API REST para backend - Google Tag Manager

Tempo estimado: 30 minutos

Nível: Intermediário 🟡


Pré-requisitos

1. Complyr

  • ✅ Workspace criado e ativo
  • ✅ Template de consentimento publicado
  • ✅ Workspace ID disponível

2. Aplicação SaaS

  • ✅ Sistema de autenticação implementado (login/registro)
  • ✅ API backend para salvar preferências de usuário
  • ✅ Banco de dados para armazenar consentimentos por usuário

3. Google Tag Manager (Opcional)

  • ✅ Container GTM configurado
  • ✅ GA4 Measurement ID

Arquitetura da Solução

flowchart TD
    A[Usuário Visita App] --> B{Está Logado?}
    B -->|Não| C[Banner Complyr Aparece]
    B -->|Sim| D{Tem Preferências Salvas?}

    C --> E[Usuário Define Consentimento]
    E --> F[Salvo em localStorage]

    D -->|Não| C
    D -->|Sim| G[Carregar Preferências do Backend]
    G --> H[Aplicar Consentimento Automaticamente]
    H --> I[Banner NÃO Aparece]

    F --> J[Usuário Faz Login]
    J --> K[Sincronizar com Backend]
    K --> L[identify email no Complyr]
    L --> M[Preferências Salvas no DB]

Passo 1: Instalação Base

1.1. Adicionar Script Complyr

No arquivo public/index.html ou componente principal, adicione o script Complyr:

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Minha Aplicação SaaS</title>
</head>
<body>
  <noscript>Você precisa habilitar JavaScript para executar esta aplicação.</noscript>
  <div id="root"></div>

  <!-- ✅ Complyr Script - Adicionar antes do </body> -->
  <script
    src="https://app.complyr.com.br/tag/js"
    data-workspace-id="SEU_WORKSPACE_ID"
    data-complyr-script
    async
    defer>
  </script>

  <!-- Google Tag Manager (Opcional) -->
  <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
  new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
  j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
  'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
  })(window,document,'script','dataLayer','GTM-XXXXXX');</script>
  <!-- End Google Tag Manager -->
</body>
</html>

Passo 2: Criar Hook Customizado useComplyr

Crie um hook React para gerenciar o Complyr:

// src/hooks/useComplyr.ts
import { useEffect, useState } from 'react';

interface ConsentPurposes {
  essential: boolean;
  analytics: boolean;
  marketing: boolean;
  personalization: boolean;
  third_party: boolean;
}

interface ConsentData {
  workspaceId: string;
  consentId: string;
  status: 'NONE' | 'GRANTED' | 'PARTIAL' | 'DENIED' | 'REVOKED' | 'EXPIRED';
  purposes: Record<string, { granted: boolean; grantedAt: string | null }>;
  createdAt: string;
  expiresAt: string;
}

export function useComplyr() {
  const [isLoaded, setIsLoaded] = useState(false);
  const [consent, setConsent] = useState<ConsentData | null>(null);

  useEffect(() => {
    // Listener para quando Complyr carregar
    const handleComplyrLoaded = () => {
      setIsLoaded(true);
      loadConsent();
    };

    // Listener para quando consentimento for atualizado
    const handleConsentUpdated = (event: CustomEvent<ConsentPurposes>) => {
      console.log('Consentimento atualizado:', event.detail);
      loadConsent();
    };

    document.addEventListener('complyr:loaded', handleComplyrLoaded as EventListener);
    document.addEventListener('consent-updated', handleConsentUpdated as EventListener);

    // Se já carregou, atualizar estado
    if (window.complyr) {
      setIsLoaded(true);
      loadConsent();
    }

    return () => {
      document.removeEventListener('complyr:loaded', handleComplyrLoaded as EventListener);
      document.removeEventListener('consent-updated', handleConsentUpdated as EventListener);
    };
  }, []);

  const loadConsent = () => {
    try {
      const stored = localStorage.getItem('complyr_consent');
      if (stored) {
        setConsent(JSON.parse(stored));
      }
    } catch (error) {
      console.error('Erro ao carregar consentimento:', error);
    }
  };

  const identify = (email: string) => {
    if (window.complyr) {
      window.complyr.identify('email', email);
    }
  };

  const openPreferences = () => {
    if (window.complyr) {
      window.complyr.openPreferences();
    }
  };

  const revokeConsent = (reason: string) => {
    if (window.complyr) {
      window.complyr.revokeConsent(reason);
    }
  };

  return {
    isLoaded,
    consent,
    identify,
    openPreferences,
    revokeConsent,
  };
}

// Tipos para window.complyr
declare global {
  interface Window {
    complyr?: {
      identify: (type: string, value: string) => void;
      openPreferences: () => void;
      revokeConsent: (reason: string) => void;
      loadPolicy: (type: string, policyId: string | null) => void;
      acceptPolicy: (type: string, policyId: string | null) => void;
    };
  }
}

Passo 3: Sincronização com Backend

3.1. Criar API Service

// src/services/api.ts
import axios from 'axios';

const api = axios.create({
  baseURL: process.env.REACT_APP_API_URL || 'http://localhost:5000',
});

// Interceptor para adicionar token JWT
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export interface SaveConsentPreferencesDto {
  analytics: boolean;
  marketing: boolean;
  personalization: boolean;
  third_party: boolean;
}

export interface UserConsentPreferences {
  userId: string;
  analytics: boolean;
  marketing: boolean;
  personalization: boolean;
  third_party: boolean;
  createdAt: string;
  updatedAt: string;
}

// Salvar preferências de consentimento no backend
export async function saveUserConsentPreferences(
  preferences: SaveConsentPreferencesDto
): Promise<UserConsentPreferences> {
  const response = await api.post('/user/consent-preferences', preferences);
  return response.data;
}

// Carregar preferências de consentimento do backend
export async function getUserConsentPreferences(): Promise<UserConsentPreferences | null> {
  try {
    const response = await api.get('/user/consent-preferences');
    return response.data;
  } catch (error) {
    if (axios.isAxiosError(error) && error.response?.status === 404) {
      return null; // Usuário ainda não tem preferências salvas
    }
    throw error;
  }
}

// Revogar consentimento
export async function revokeUserConsent(reason: string): Promise<void> {
  await api.post('/user/revoke-consent', { reason });
}

export default api;

3.2. Implementar Endpoint no Backend (Exemplo NestJS)

// backend/src/user/consent-preferences.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { CurrentUser } from '../auth/current-user.decorator';

interface ConsentPreferencesDto {
  analytics: boolean;
  marketing: boolean;
  personalization: boolean;
  third_party: boolean;
}

@Controller('user')
@UseGuards(JwtAuthGuard)
export class ConsentPreferencesController {
  constructor(private readonly consentService: ConsentPreferencesService) {}

  @Post('consent-preferences')
  async savePreferences(
    @CurrentUser() user: User,
    @Body() dto: ConsentPreferencesDto,
  ) {
    return this.consentService.savePreferences(user.id, dto);
  }

  @Get('consent-preferences')
  async getPreferences(@CurrentUser() user: User) {
    const preferences = await this.consentService.getPreferences(user.id);
    if (!preferences) {
      throw new NotFoundException('Preferences not found');
    }
    return preferences;
  }

  @Post('revoke-consent')
  async revokeConsent(
    @CurrentUser() user: User,
    @Body('reason') reason: string,
  ) {
    return this.consentService.revokeConsent(user.id, reason);
  }
}

Tabela no Banco de Dados (PostgreSQL):

CREATE TABLE user_consent_preferences (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  analytics BOOLEAN DEFAULT false,
  marketing BOOLEAN DEFAULT false,
  personalization BOOLEAN DEFAULT false,
  third_party BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE(user_id)
);

CREATE INDEX idx_user_consent_user_id ON user_consent_preferences(user_id);


Passo 4: Context Provider para Autenticação

// src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useComplyr } from '../hooks/useComplyr';
import { getUserConsentPreferences } from '../services/api';

interface User {
  id: string;
  email: string;
  name: string;
}

interface AuthContextData {
  user: User | null;
  loading: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextData>({} as AuthContextData);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const { identify } = useComplyr();

  useEffect(() => {
    // Carregar usuário do localStorage
    const storedUser = localStorage.getItem('user');
    if (storedUser) {
      const parsedUser = JSON.parse(storedUser);
      setUser(parsedUser);

      // Identificar no Complyr
      identify(parsedUser.email);

      // Carregar preferências do backend
      loadUserPreferences();
    }
    setLoading(false);
  }, []);

  const loadUserPreferences = async () => {
    try {
      const preferences = await getUserConsentPreferences();
      if (preferences) {
        // Aplicar preferências automaticamente
        // O Complyr vai ler do localStorage ou do backend
        console.log('Preferências carregadas:', preferences);
      }
    } catch (error) {
      console.error('Erro ao carregar preferências:', error);
    }
  };

  const login = async (email: string, password: string) => {
    // Sua lógica de login aqui
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    const data = await response.json();

    if (data.success) {
      const userData = data.user;
      setUser(userData);
      localStorage.setItem('user', JSON.stringify(userData));
      localStorage.setItem('auth_token', data.token);

      // ✅ IMPORTANTE: Identificar no Complyr após login
      identify(userData.email);

      // Carregar preferências do backend
      await loadUserPreferences();
    }
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
    localStorage.removeItem('auth_token');
    localStorage.removeItem('complyr_consent');
  };

  return (
    <AuthContext.Provider value={{ user, loading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

Passo 5: Componente de Configurações de Privacidade

Crie uma página para o usuário gerenciar suas preferências:

// src/pages/PrivacySettings.tsx
import React, { useState, useEffect } from 'react';
import { useComplyr } from '../hooks/useComplyr';
import { saveUserConsentPreferences, getUserConsentPreferences, revokeUserConsent } from '../services/api';

export function PrivacySettings() {
  const { consent, openPreferences, isLoaded } = useComplyr();
  const [preferences, setPreferences] = useState({
    analytics: false,
    marketing: false,
    personalization: false,
    third_party: false,
  });
  const [loading, setLoading] = useState(true);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    loadPreferences();
  }, []);

  const loadPreferences = async () => {
    try {
      const userPrefs = await getUserConsentPreferences();
      if (userPrefs) {
        setPreferences({
          analytics: userPrefs.analytics,
          marketing: userPrefs.marketing,
          personalization: userPrefs.personalization,
          third_party: userPrefs.third_party,
        });
      } else if (consent) {
        // Fallback: usar localStorage
        setPreferences({
          analytics: consent.purposes.analytics?.granted || false,
          marketing: consent.purposes.marketing?.granted || false,
          personalization: consent.purposes.personalization?.granted || false,
          third_party: consent.purposes.third_party?.granted || false,
        });
      }
    } catch (error) {
      console.error('Erro ao carregar preferências:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleSave = async () => {
    setSaving(true);
    try {
      await saveUserConsentPreferences(preferences);
      alert('Preferências salvas com sucesso!');
    } catch (error) {
      console.error('Erro ao salvar preferências:', error);
      alert('Erro ao salvar preferências. Tente novamente.');
    } finally {
      setSaving(false);
    }
  };

  const handleRevoke = async () => {
    if (window.confirm('Deseja revogar todos os seus consentimentos?')) {
      try {
        await revokeUserConsent('User requested data deletion');
        setPreferences({
          analytics: false,
          marketing: false,
          personalization: false,
          third_party: false,
        });
        alert('Consentimentos revogados com sucesso!');
      } catch (error) {
        console.error('Erro ao revogar consentimento:', error);
        alert('Erro ao revogar consentimento. Tente novamente.');
      }
    }
  };

  if (loading) {
    return <div>Carregando...</div>;
  }

  return (
    <div className="privacy-settings">
      <h1>Configurações de Privacidade</h1>
      <p>Gerencie suas preferências de cookies e dados pessoais.</p>

      <div className="consent-options">
        {/* Essencial - sempre ativo */}
        <div className="consent-item">
          <div className="consent-header">
            <h3>🔒 Cookies Essenciais</h3>
            <span className="badge">Sempre Ativo</span>
          </div>
          <p>
            Necessários para o funcionamento básico da aplicação (autenticação, sessão, segurança).
            Não podem ser desativados.
          </p>
        </div>

        {/* Analytics */}
        <div className="consent-item">
          <div className="consent-header">
            <h3>📊 Analytics</h3>
            <label className="toggle">
              <input
                type="checkbox"
                checked={preferences.analytics}
                onChange={(e) => setPreferences({ ...preferences, analytics: e.target.checked })}
              />
              <span className="slider"></span>
            </label>
          </div>
          <p>
            Nos ajudam a entender como você usa a aplicação para melhorar a experiência.
            Google Analytics, Hotjar, etc.
          </p>
        </div>

        {/* Marketing */}
        <div className="consent-item">
          <div className="consent-header">
            <h3>📢 Marketing</h3>
            <label className="toggle">
              <input
                type="checkbox"
                checked={preferences.marketing}
                onChange={(e) => setPreferences({ ...preferences, marketing: e.target.checked })}
              />
              <span className="slider"></span>
            </label>
          </div>
          <p>
            Usados para exibir anúncios relevantes e medir a eficácia de campanhas.
            Facebook Pixel, Google Ads, etc.
          </p>
        </div>

        {/* Personalization */}
        <div className="consent-item">
          <div className="consent-header">
            <h3>🎨 Personalização</h3>
            <label className="toggle">
              <input
                type="checkbox"
                checked={preferences.personalization}
                onChange={(e) => setPreferences({ ...preferences, personalization: e.target.checked })}
              />
              <span className="slider"></span>
            </label>
          </div>
          <p>
            Permitem personalizar a interface e conteúdo baseado nas suas preferências.
          </p>
        </div>

        {/* Third Party */}
        <div className="consent-item">
          <div className="consent-header">
            <h3>🔗 Terceiros</h3>
            <label className="toggle">
              <input
                type="checkbox"
                checked={preferences.third_party}
                onChange={(e) => setPreferences({ ...preferences, third_party: e.target.checked })}
              />
              <span className="slider"></span>
            </label>
          </div>
          <p>
            Serviços de terceiros como chat, vídeos incorporados, mapas, etc.
          </p>
        </div>
      </div>

      {/* Botões de ação */}
      <div className="actions">
        <button
          className="btn btn-primary"
          onClick={handleSave}
          disabled={saving}
        >
          {saving ? 'Salvando...' : 'Salvar Preferências'}
        </button>

        <button
          className="btn btn-secondary"
          onClick={openPreferences}
          disabled={!isLoaded}
        >
          Abrir Banner Complyr
        </button>

        <button
          className="btn btn-danger"
          onClick={handleRevoke}
        >
          Revogar Todos os Consentimentos
        </button>
      </div>

      {/* Informações adicionais */}
      <div className="info-section">
        <h3>Seus Direitos (LGPD)</h3>
        <ul>
          <li>Confirmação de processamento de dados</li>
          <li>Acesso aos seus dados</li>
          <li>Correção de dados incompletos ou desatualizados</li>
          <li>Anonimização, bloqueio ou eliminação</li>
          <li>Portabilidade dos dados</li>
          <li>Informações sobre compartilhamento</li>
          <li>Revogação de consentimento</li>
        </ul>

        <p>
          Para exercer seus direitos, entre em contato: <a href="mailto:privacidade@suaapp.com">privacidade@suaapp.com</a>
        </p>
      </div>
    </div>
  );
}

CSS para Toggle Switches:

/* src/styles/privacy-settings.css */
.privacy-settings {
  max-width: 800px;
  margin: 0 auto;
  padding: 40px 20px;
}

.consent-options {
  display: flex;
  flex-direction: column;
  gap: 20px;
  margin: 30px 0;
}

.consent-item {
  background: #f9f9f9;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 20px;
}

.consent-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}

.consent-header h3 {
  margin: 0;
  font-size: 18px;
}

.badge {
  background: #4CAF50;
  color: white;
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
}

/* Toggle Switch */
.toggle {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 24px;
}

.toggle input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  transition: 0.3s;
  border-radius: 24px;
}

.slider:before {
  position: absolute;
  content: "";
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background-color: white;
  transition: 0.3s;
  border-radius: 50%;
}

input:checked + .slider {
  background-color: #4CAF50;
}

input:checked + .slider:before {
  transform: translateX(26px);
}

/* Botões */
.actions {
  display: flex;
  gap: 12px;
  margin-top: 30px;
}

.btn {
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-primary {
  background: #4CAF50;
  color: white;
}

.btn-primary:hover:not(:disabled) {
  background: #45a049;
}

.btn-secondary {
  background: #2196F3;
  color: white;
}

.btn-secondary:hover:not(:disabled) {
  background: #0b7dda;
}

.btn-danger {
  background: #f44336;
  color: white;
}

.btn-danger:hover {
  background: #da190b;
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.info-section {
  margin-top: 40px;
  padding: 20px;
  background: #fff3cd;
  border-left: 4px solid #ffc107;
  border-radius: 4px;
}

.info-section h3 {
  margin-top: 0;
}

.info-section ul {
  margin: 10px 0;
  padding-left: 20px;
}

.info-section li {
  margin: 8px 0;
}


Passo 6: Integração com Google Tag Manager

6.1. Rastrear Login

// src/pages/Login.tsx
import { useAuth } from '../contexts/AuthContext';

export function Login() {
  const { login } = useAuth();

  const handleLogin = async (email: string, password: string) => {
    await login(email, password);

    // Rastrear login no GTM
    if (window.dataLayer) {
      window.dataLayer.push({
        event: 'login',
        method: 'email',
        user_id: email, // Será hasheado pelo Complyr
      });
    }
  };

  // ...resto do componente
}

6.2. Rastrear Eventos de Produto (se aplicável)

// src/components/Product.tsx
import { useComplyr } from '../hooks/useComplyr';

export function Product({ product }) {
  const { consent } = useComplyr();

  const trackProductView = () => {
    if (consent?.purposes.analytics?.granted && window.dataLayer) {
      window.dataLayer.push({
        event: 'view_item',
        ecommerce: {
          items: [{
            item_id: product.id,
            item_name: product.name,
            price: product.price,
          }]
        }
      });
    }
  };

  useEffect(() => {
    trackProductView();
  }, [product]);

  // ...resto do componente
}

Passo 7: Banner Condicional

Evite exibir banner para usuários logados com preferências salvas:

// src/App.tsx
import { useEffect } from 'react';
import { useAuth } from './contexts/AuthContext';
import { getUserConsentPreferences } from './services/api';

export function App() {
  const { user } = useAuth();

  useEffect(() => {
    checkUserPreferences();
  }, [user]);

  const checkUserPreferences = async () => {
    if (user) {
      // Usuário logado - verificar se tem preferências
      const preferences = await getUserConsentPreferences();

      if (preferences) {
        // Usuário tem preferências salvas
        // Aplicar automaticamente sem exibir banner
        applyPreferencesAutomatically(preferences);
      }
      // Se não tiver preferências, banner aparecerá normalmente
    }
  };

  const applyPreferencesAutomatically = (preferences: any) => {
    // Simular aceite/rejeição baseado nas preferências salvas
    const consentData = {
      analytics: preferences.analytics,
      marketing: preferences.marketing,
      personalization: preferences.personalization,
      third_party: preferences.third_party,
    };

    // Disparar evento consent-updated
    document.dispatchEvent(
      new CustomEvent('consent-updated', { detail: consentData })
    );

    // Atualizar dataLayer para GTM
    if (window.dataLayer) {
      window.dataLayer.push({
        event: 'consent-updated',
        consent_analytics: preferences.analytics,
        consent_marketing: preferences.marketing,
        consent_personalization: preferences.personalization,
        consent_third_party: preferences.third_party,
      });
    }
  };

  return (
    // ...seu app
  );
}

Exemplo Completo - App.tsx

// src/App.tsx
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { Login } from './pages/Login';
import { Register } from './pages/Register';
import { Dashboard } from './pages/Dashboard';
import { PrivacySettings } from './pages/PrivacySettings';

function PrivateRoute({ children }: { children: React.ReactNode }) {
  const { user, loading } = useAuth();

  if (loading) {
    return <div>Carregando...</div>;
  }

  return user ? <>{children}</> : <Navigate to="/login" />;
}

function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          {/* Rotas públicas */}
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />

          {/* Rotas privadas */}
          <Route
            path="/dashboard"
            element={
              <PrivateRoute>
                <Dashboard />
              </PrivateRoute>
            }
          />
          <Route
            path="/settings/privacy"
            element={
              <PrivateRoute>
                <PrivacySettings />
              </PrivateRoute>
            }
          />

          {/* Redirect padrão */}
          <Route path="/" element={<Navigate to="/dashboard" />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

export default App;

Testes

1. Testar Identificação de Usuário

// Console do navegador após login
console.log(localStorage.getItem('complyr_consent'));
// Deve conter emailHash

// Verificar se identify foi chamado
// DevTools → Network → Filtrar por "complyr"
// Deve haver uma requisição POST com hash do email

2. Testar Sincronização Cross-Device

  1. Dispositivo 1 (Desktop):
  2. Faça login
  3. Aceite apenas Analytics
  4. Salve preferências

  5. Dispositivo 2 (Mobile):

  6. Faça login com mesmo usuário
  7. Preferências devem ser carregadas automaticamente
  8. Banner NÃO deve aparecer

3. Testar Revogação

  1. Vá em Configurações de Privacidade
  2. Clique em "Revogar Todos os Consentimentos"
  3. Verifique:
  4. Preferências zeradas no backend
  5. localStorage atualizado
  6. Analytics/Marketing desabilitados

Problemas Comuns

Problema 1: Banner aparece para usuário logado

Causa: Preferências não sendo carregadas do backend.

Solução:

// Verificar se endpoint está retornando dados
const prefs = await getUserConsentPreferences();
console.log('Preferências carregadas:', prefs);

// Se null, usuário não tem preferências salvas ainda
// Banner deve aparecer normalmente

Problema 2: identify() não funciona

Causa: Script Complyr não carregou ou email inválido.

Solução:

// Aguardar carregamento do script
document.addEventListener('complyr:loaded', () => {
  if (window.complyr) {
    window.complyr.identify('email', user.email);
  }
});

Problema 3: Preferências não sincronizam

Causa: Token JWT ausente ou inválido.

Solução:

// Verificar se token está sendo enviado
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  console.log('Token JWT:', token);
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});


Checklist de Validação

  • Script Complyr instalado no index.html
  • Hook useComplyr implementado
  • Context AuthProvider configurado
  • Endpoint backend /user/consent-preferences criado
  • Tabela user_consent_preferences criada no DB
  • Função identify() chamada após login
  • Página de Configurações de Privacidade criada
  • Toggle switches funcionam corretamente
  • Salvar preferências persiste no backend
  • Preferências carregadas automaticamente após login
  • Banner NÃO aparece para usuários com preferências
  • Revogação de consentimento funciona
  • GTM integrado (se aplicável)
  • User ID rastreado no GA4
  • Testes em múltiplos dispositivos OK

Próximos Passos

  1. React SPA Avançado - Hooks, Context, TypeScript
  2. Google Tag Manager - Rastreamento avançado
  3. API JavaScript - Métodos disponíveis
  4. LGPD Compliance - Conformidade legal

Conclusão

Parabéns! 🎉 Você implementou uma solução completa de consentimento para aplicação SaaS com:

  • ✅ Identificação de usuários
  • ✅ Sincronização cross-device
  • ✅ Preferências persistidas
  • ✅ Gestão na conta do usuário
  • ✅ Banner condicional

Agora seus usuários têm controle total sobre seus dados e você está em conformidade com LGPD!

Suporte: contato@complyr.com.br