diff --git a/src/components/onboarding/OnboardingTour.tsx b/src/components/onboarding/OnboardingTour.tsx index 89dcf33..ea4818e 100644 --- a/src/components/onboarding/OnboardingTour.tsx +++ b/src/components/onboarding/OnboardingTour.tsx @@ -1,5 +1,5 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { X } from 'lucide-react'; @@ -19,7 +19,30 @@ const OnboardingTour: React.FC = ({ onSkip, onClose }) => { - if (!isOpen) return null; + const cleanupRef = useRef<() => void>(); + + // Função para remover estilos de highlight + const removeHighlightStyle = () => { + const elements = document.querySelectorAll('[data-tour]') as NodeListOf; + elements.forEach(element => { + element.style.backgroundColor = ''; + element.style.color = ''; + element.style.fontWeight = ''; + element.style.boxShadow = ''; + element.style.border = ''; + element.style.borderRadius = ''; + element.style.position = ''; + element.style.zIndex = ''; + }); + }; + + if (!isOpen) { + // Limpar estilos quando o tour não estiver aberto + useEffect(() => { + removeHighlightStyle(); + }, []); + return null; + } const steps = [ { @@ -74,110 +97,7 @@ const OnboardingTour: React.FC = ({ const currentStepData = steps[currentStep]; - const getSpotlightStyle = () => { - if (!currentStepData.spotlight) return {}; - - const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`); - if (!element) return {}; - - const rect = element.getBoundingClientRect(); - return { - position: 'absolute' as const, - left: rect.left - 10, - top: rect.top - 10, - width: rect.width + 20, - height: rect.height + 20, - borderRadius: '8px', - boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.8), 0 0 30px rgba(59, 130, 246, 1), inset 0 0 0 3px rgba(59, 130, 246, 0.8)', - pointerEvents: 'none' as const, - zIndex: 9998, - border: '2px solid rgba(59, 130, 246, 0.9)' - }; - }; - - const getArrowStyle = () => { - if (!currentStepData.spotlight) return { display: 'none' }; - - const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`); - const card = document.querySelector('.onboarding-card'); - - if (!element || !card) return { display: 'none' }; - - const elementRect = element.getBoundingClientRect(); - const cardRect = card.getBoundingClientRect(); - - // Calcular posição da seta - const elementCenterX = elementRect.left + elementRect.width / 2; - const elementCenterY = elementRect.top + elementRect.height / 2; - const cardCenterX = cardRect.left + cardRect.width / 2; - const cardCenterY = cardRect.top + cardRect.height / 2; - - // Calcular ângulo e distância - const deltaX = elementCenterX - cardCenterX; - const deltaY = elementCenterY - cardCenterY; - const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); - const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); - - // Posicionar a seta no meio do caminho - const arrowX = cardCenterX + (deltaX * 0.6); - const arrowY = cardCenterY + (deltaY * 0.6); - - return { - position: 'fixed' as const, - left: arrowX, - top: arrowY, - transform: `translate(-50%, -50%) rotate(${angle}deg)`, - zIndex: 9999, - pointerEvents: 'none' as const, - width: '60px', - height: '3px', - background: 'linear-gradient(90deg, rgba(59, 130, 246, 0.8) 0%, rgba(59, 130, 246, 0.4) 100%)', - borderRadius: '2px', - filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))', - }; - }; - - const getArrowHeadStyle = () => { - if (!currentStepData.spotlight) return { display: 'none' }; - - const element = document.querySelector(`[data-tour="${currentStepData.spotlight}"]`); - const card = document.querySelector('.onboarding-card'); - - if (!element || !card) return { display: 'none' }; - - const elementRect = element.getBoundingClientRect(); - const cardRect = card.getBoundingClientRect(); - - const elementCenterX = elementRect.left + elementRect.width / 2; - const elementCenterY = elementRect.top + elementRect.height / 2; - const cardCenterX = cardRect.left + cardRect.width / 2; - const cardCenterY = cardRect.top + cardRect.height / 2; - - const deltaX = elementCenterX - cardCenterX; - const deltaY = elementCenterY - cardCenterY; - const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); - - // Posicionar a ponta da seta mais próxima do elemento - const arrowHeadX = cardCenterX + (deltaX * 0.75); - const arrowHeadY = cardCenterY + (deltaY * 0.75); - - return { - position: 'fixed' as const, - left: arrowHeadX, - top: arrowHeadY, - transform: `translate(-50%, -50%) rotate(${angle}deg)`, - zIndex: 9999, - pointerEvents: 'none' as const, - width: '0', - height: '0', - borderLeft: '8px solid rgba(59, 130, 246, 0.8)', - borderTop: '6px solid transparent', - borderBottom: '6px solid transparent', - filter: 'drop-shadow(0 2px 4px rgba(59, 130, 246, 0.3))', - }; - }; - - // Estilo para destacar o elemento com fundo branco e texto preto + // Aplicar highlight no elemento const highlightElementStyle = () => { if (!currentStepData.spotlight) return; @@ -195,37 +115,37 @@ const OnboardingTour: React.FC = ({ element.style.zIndex = '9999'; }; - // Remover estilos quando não há spotlight - const removeHighlightStyle = () => { - const elements = document.querySelectorAll('[data-tour]') as NodeListOf; - elements.forEach(element => { - element.style.backgroundColor = ''; - element.style.color = ''; - element.style.fontWeight = ''; - element.style.boxShadow = ''; - element.style.border = ''; - element.style.borderRadius = ''; - element.style.position = ''; - element.style.zIndex = ''; - }); - }; - // Aplicar ou remover highlight baseado no step atual - React.useEffect(() => { + useEffect(() => { + // Limpar estilos anteriores + removeHighlightStyle(); + if (currentStepData.spotlight) { // Pequeno delay para garantir que o DOM foi renderizado - setTimeout(() => { + const timer = setTimeout(() => { highlightElementStyle(); }, 100); - } else { - removeHighlightStyle(); + + cleanupRef.current = () => { + clearTimeout(timer); + removeHighlightStyle(); + }; } - // Cleanup quando o componente for desmontado + // Cleanup quando o componente for desmontado ou step mudar + return () => { + if (cleanupRef.current) { + cleanupRef.current(); + } + }; + }, [currentStep, currentStepData.spotlight]); + + // Cleanup quando o componente for desmontado + useEffect(() => { return () => { removeHighlightStyle(); }; - }, [currentStep, currentStepData.spotlight]); + }, []); return ( <> @@ -235,19 +155,6 @@ const OnboardingTour: React.FC = ({ style={{ zIndex: 9997 }} /> - {/* Spotlight transparente */} - {currentStepData.spotlight && ( -
- )} - - {/* Seta moderna conectando o card ao elemento */} - {currentStepData.spotlight && ( - <> -
-
- - )} - {/* Card do tour */}
diff --git a/src/hooks/useOnboardingTour.ts b/src/hooks/useOnboardingTour.ts index 32de93e..4328402 100644 --- a/src/hooks/useOnboardingTour.ts +++ b/src/hooks/useOnboardingTour.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import { listWhatsAppGroups } from '@/services/whatsAppGroupsService'; @@ -9,11 +9,20 @@ export const useOnboardingTour = () => { const [isOpen, setIsOpen] = useState(false); const [currentStep, setCurrentStep] = useState(0); const [shouldShowTour, setShouldShowTour] = useState(false); - const [tourShownThisSession, setTourShownThisSession] = useState(false); const location = useLocation(); + const tourCheckedRef = useRef(false); + const isCheckingRef = useRef(false); // Verificar se o tour deve ser exibido const checkTourConditions = async () => { + // Evitar múltiplas verificações simultâneas + if (isCheckingRef.current) { + console.log('🔍 [TOUR] Verificação já em andamento, pulando...'); + return; + } + + isCheckingRef.current = true; + try { console.log('🔍 [TOUR] Iniciando verificação de condições do tour...'); @@ -21,6 +30,7 @@ export const useOnboardingTour = () => { if (location.pathname !== '/') { console.log('❌ [TOUR] Tour só aparece no dashboard, página atual:', location.pathname); setShouldShowTour(false); + isCheckingRef.current = false; return; } @@ -31,10 +41,20 @@ export const useOnboardingTour = () => { if (!userEmail) { console.log('❌ [TOUR] Email não encontrado no localStorage'); setShouldShowTour(false); + isCheckingRef.current = false; return; } - // SEMPRE verificar se há grupos cadastrados (ignorar sessionStorage inicialmente) + // Verificar se já foi mostrado nesta sessão + const shownThisSession = sessionStorage.getItem(TOUR_SESSION_KEY) === 'true'; + if (shownThisSession) { + console.log('❌ [TOUR] Tour já foi exibido nesta sessão'); + setShouldShowTour(false); + isCheckingRef.current = false; + return; + } + + // Verificar se há grupos cadastrados let hasGroups = false; try { @@ -54,18 +74,7 @@ export const useOnboardingTour = () => { if (hasGroups) { console.log('✅ [TOUR] Usuário já tem grupos, não mostrar tour'); setShouldShowTour(false); - setIsOpen(false); - return; - } - - // Se não tem grupos, verificar se já foi mostrado nesta sessão - const shownThisSession = sessionStorage.getItem(TOUR_SESSION_KEY) === 'true'; - console.log('🔍 [TOUR] Usuário SEM grupos. Verificando se tour já foi exibido nesta sessão:', shownThisSession); - - if (shownThisSession) { - console.log('❌ [TOUR] Tour já foi exibido nesta sessão para este usuário sem grupos'); - setShouldShowTour(false); - setTourShownThisSession(true); + isCheckingRef.current = false; return; } @@ -76,17 +85,19 @@ export const useOnboardingTour = () => { // Abrir tour automaticamente após um pequeno delay console.log('🚀 [TOUR] Abrindo tour automaticamente - usuário sem grupos'); setTimeout(() => { - console.log('🎬 [TOUR] Executando abertura do tour...'); - setIsOpen(true); - setCurrentStep(0); - // SÓ marcar como exibido quando realmente abrir - sessionStorage.setItem(TOUR_SESSION_KEY, 'true'); - setTourShownThisSession(true); + if (!sessionStorage.getItem(TOUR_SESSION_KEY)) { + console.log('🎬 [TOUR] Executando abertura do tour...'); + setIsOpen(true); + setCurrentStep(0); + sessionStorage.setItem(TOUR_SESSION_KEY, 'true'); + } }, 2000); } catch (error) { console.error('❌ [TOUR] Erro ao verificar condições do tour:', error); setShouldShowTour(false); + } finally { + isCheckingRef.current = false; } }; @@ -96,17 +107,20 @@ export const useOnboardingTour = () => { setIsOpen(false); setShouldShowTour(false); sessionStorage.removeItem(TOUR_SESSION_KEY); - setTourShownThisSession(false); + tourCheckedRef.current = false; }; // Verificar condições quando a localização mudar ou na inicialização useEffect(() => { - console.log('🔄 [TOUR] useEffect disparado - verificando condições do tour'); - const timer = setTimeout(() => { - checkTourConditions(); - }, 1000); + if (!tourCheckedRef.current && location.pathname === '/') { + console.log('🔄 [TOUR] useEffect disparado - verificando condições do tour'); + tourCheckedRef.current = true; + const timer = setTimeout(() => { + checkTourConditions(); + }, 1000); - return () => clearTimeout(timer); + return () => clearTimeout(timer); + } }, [location.pathname]); // Escutar evento customizado de login @@ -115,9 +129,8 @@ export const useOnboardingTour = () => { console.log('🎉 [TOUR] Evento de login recebido:', event.detail); if (location.pathname === '/') { console.log('📧 [TOUR] Login detectado no dashboard, re-verificando condições do tour'); - // Limpar sessionStorage para nova verificação após login + tourCheckedRef.current = false; sessionStorage.removeItem(TOUR_SESSION_KEY); - setTourShownThisSession(false); setTimeout(() => { checkTourConditions(); }, 2500); @@ -130,41 +143,6 @@ export const useOnboardingTour = () => { }; }, [location.pathname]); - // Verificar também quando o email do usuário estiver disponível - useEffect(() => { - const userEmail = localStorage.getItem('userEmail'); - console.log('📧 [TOUR] Segundo useEffect - email disponível:', userEmail); - if (userEmail && location.pathname === '/') { - console.log('📧 [TOUR] Email detectado, re-verificando condições do tour'); - // Limpar sessionStorage para nova verificação - sessionStorage.removeItem(TOUR_SESSION_KEY); - setTourShownThisSession(false); - setTimeout(() => { - checkTourConditions(); - }, 1500); - } - }, []); - - // Escutar mudanças no localStorage para detectar login - useEffect(() => { - const handleStorageChange = (e: StorageEvent) => { - if (e.key === 'userEmail' && e.newValue) { - console.log('👤 [TOUR] Login detectado via storage event:', e.newValue); - if (location.pathname === '/') { - // Limpar sessionStorage para nova verificação após mudança de usuário - sessionStorage.removeItem(TOUR_SESSION_KEY); - setTourShownThisSession(false); - setTimeout(() => { - checkTourConditions(); - }, 2000); - } - } - }; - - window.addEventListener('storage', handleStorageChange); - return () => window.removeEventListener('storage', handleStorageChange); - }, [location.pathname]); - const nextStep = () => { console.log('➡️ [TOUR] Próximo step do tour:', currentStep + 1); if (currentStep < 2) { @@ -183,16 +161,15 @@ export const useOnboardingTour = () => { console.log('❌ [TOUR] Fechando tour'); setIsOpen(false); setCurrentStep(0); - // Marcar como exibido nesta sessão apenas quando fechar + setShouldShowTour(false); sessionStorage.setItem(TOUR_SESSION_KEY, 'true'); - setTourShownThisSession(true); }; // Função para forçar reabrir o tour (para testes) const reopenTour = () => { console.log('🔄 [TOUR] Reabrindo tour manualmente'); sessionStorage.removeItem(TOUR_SESSION_KEY); - setTourShownThisSession(false); + tourCheckedRef.current = false; setIsOpen(true); setCurrentStep(0); }; diff --git a/src/pages/Assinatura.tsx b/src/pages/Assinatura.tsx index 2ca129f..b18b5f1 100644 --- a/src/pages/Assinatura.tsx +++ b/src/pages/Assinatura.tsx @@ -44,6 +44,7 @@ const Assinatura = () => { }); if (error) { + console.error("Erro completo:", error); throw new Error(`Erro de comunicação: ${error.message}`); } @@ -52,6 +53,7 @@ const Assinatura = () => { } if (data.init_point) { + console.log("Redirecionando para:", data.init_point); window.location.href = data.init_point; } else { throw new Error("Não foi possível obter a URL de checkout. Tente novamente."); @@ -59,7 +61,13 @@ const Assinatura = () => { } catch (error: any) { console.error("Erro ao criar a assinatura:", error); - toast.error(error.message || "Ocorreu um erro inesperado. Por favor, tente mais tarde."); + + // Mensagem mais amigável para erro de região + if (error.message?.includes('temporariamente indisponível')) { + toast.error("Serviço temporariamente indisponível para sua região. Tente novamente em alguns minutos."); + } else { + toast.error(error.message || "Ocorreu um erro inesperado. Por favor, tente mais tarde."); + } } finally { setIsSubscribing(false); } diff --git a/supabase/functions/mercado-pago-subscribe/index.ts b/supabase/functions/mercado-pago-subscribe/index.ts index 65b1ba6..11f0e71 100644 --- a/supabase/functions/mercado-pago-subscribe/index.ts +++ b/supabase/functions/mercado-pago-subscribe/index.ts @@ -46,7 +46,6 @@ serve(async (req) => { }); } - const MERCADO_PAGO_ACCESS_TOKEN = Deno.env.get('MERCADO_PAGO_ACCESS_TOKEN') if (!MERCADO_PAGO_ACCESS_TOKEN) { return new Response(JSON.stringify({ error: 'Chave de acesso do Mercado Pago não configurada no servidor.' }), { @@ -67,6 +66,8 @@ serve(async (req) => { payer_email: email }; + console.log('Enviando requisição para MercadoPago:', JSON.stringify(body)); + const mpResponse = await fetch("https://api.mercadopago.com/preapproval", { method: "POST", headers: { @@ -77,9 +78,21 @@ serve(async (req) => { }); const mpData = await mpResponse.json(); + console.log('Resposta do MercadoPago:', mpData); if (!mpResponse.ok) { console.error('Erro da API do Mercado Pago:', mpData); + + // Tratar erro específico de países diferentes + if (mpData.message === "Cannot operate between different countries") { + return new Response(JSON.stringify({ + error: 'Serviço temporariamente indisponível para sua região. Tente novamente mais tarde.' + }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 503, + }); + } + throw new Error(mpData.message || "Erro ao criar a assinatura no Mercado Pago."); } @@ -93,6 +106,7 @@ serve(async (req) => { }) } catch (error) { + console.error('Erro geral:', error); return new Response(JSON.stringify({ error: error.message }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500,