From fa8bee0e36dcf06a3bbb9e8dc2acc9decf843b91 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Fri, 17 Apr 2026 17:02:12 +0000
Subject: [PATCH] Changes
Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
---
src/components/mika/AuthCard.tsx | 38 +++++++++++
src/components/mika/GoogleButton.tsx | 55 +++++++++++++++
src/components/mika/PasswordStrengthMeter.tsx | 36 ++++++++++
src/hooks/use-auth.ts | 34 ++++++++++
src/hooks/use-profile.ts | 68 +++++++++++++++++++
src/lib/auth-errors.ts | 26 +++++++
src/lib/password.ts | 14 ++++
7 files changed, 271 insertions(+)
create mode 100644 src/components/mika/AuthCard.tsx
create mode 100644 src/components/mika/GoogleButton.tsx
create mode 100644 src/components/mika/PasswordStrengthMeter.tsx
create mode 100644 src/hooks/use-auth.ts
create mode 100644 src/hooks/use-profile.ts
create mode 100644 src/lib/auth-errors.ts
create mode 100644 src/lib/password.ts
diff --git a/src/components/mika/AuthCard.tsx b/src/components/mika/AuthCard.tsx
new file mode 100644
index 0000000..12fcb0b
--- /dev/null
+++ b/src/components/mika/AuthCard.tsx
@@ -0,0 +1,38 @@
+import { Link } from "@tanstack/react-router";
+import { Logo } from "./Logo";
+import { ThemeToggle } from "./ThemeToggle";
+
+export function AuthCard({
+ title,
+ subtitle,
+ children,
+ footer,
+}: {
+ title: string;
+ subtitle?: string;
+ children: React.ReactNode;
+ footer?: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+
+
{title}
+ {subtitle &&
{subtitle}
}
+
+ {children}
+
+ {footer &&
{footer}
}
+
+
+
+ );
+}
diff --git a/src/components/mika/GoogleButton.tsx b/src/components/mika/GoogleButton.tsx
new file mode 100644
index 0000000..59c87b7
--- /dev/null
+++ b/src/components/mika/GoogleButton.tsx
@@ -0,0 +1,55 @@
+"use client";
+
+import { useState } from "react";
+import { toast } from "sonner";
+import { Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { lovable } from "@/integrations/lovable/index";
+import { translateAuthError } from "@/lib/auth-errors";
+
+export function GoogleButton({ redirectTo }: { redirectTo?: string }) {
+ const [loading, setLoading] = useState(false);
+
+ const onClick = async () => {
+ setLoading(true);
+ try {
+ const url = redirectTo
+ ? `${window.location.origin}${redirectTo}`
+ : window.location.origin;
+ const result = await lovable.auth.signInWithOAuth("google", { redirect_uri: url });
+ if (result.error) {
+ toast.error(translateAuthError(result.error.message));
+ setLoading(false);
+ return;
+ }
+ if (result.redirected) return;
+ // tokens recebidos: redireciona
+ window.location.href = url;
+ } catch (e) {
+ toast.error(translateAuthError(e instanceof Error ? e.message : String(e)));
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/mika/PasswordStrengthMeter.tsx b/src/components/mika/PasswordStrengthMeter.tsx
new file mode 100644
index 0000000..1c5fe85
--- /dev/null
+++ b/src/components/mika/PasswordStrengthMeter.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { passwordStrength, passwordStrengthLabel } from "@/lib/password";
+import { cn } from "@/lib/utils";
+
+export function PasswordStrengthMeter({ password }: { password: string }) {
+ if (!password) return null;
+ const score = passwordStrength(password);
+ const colors = [
+ "bg-destructive",
+ "bg-destructive",
+ "bg-warning",
+ "bg-success",
+ ];
+ return (
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+
+ Força: {passwordStrengthLabel(score)}
+
+
+ );
+}
diff --git a/src/hooks/use-auth.ts b/src/hooks/use-auth.ts
new file mode 100644
index 0000000..6522992
--- /dev/null
+++ b/src/hooks/use-auth.ts
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import type { Session, User } from "@supabase/supabase-js";
+import { supabase } from "@/integrations/supabase/client";
+
+interface AuthState {
+ user: User | null;
+ session: Session | null;
+ loading: boolean;
+}
+
+export function useAuth(): AuthState {
+ const [state, setState] = useState({
+ user: null,
+ session: null,
+ loading: true,
+ });
+
+ useEffect(() => {
+ // CRITICAL: subscribe BEFORE getSession (avoid missed events)
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
+ setState({ user: session?.user ?? null, session, loading: false });
+ });
+
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ setState({ user: session?.user ?? null, session, loading: false });
+ });
+
+ return () => subscription.unsubscribe();
+ }, []);
+
+ return state;
+}
diff --git a/src/hooks/use-profile.ts b/src/hooks/use-profile.ts
new file mode 100644
index 0000000..7855eec
--- /dev/null
+++ b/src/hooks/use-profile.ts
@@ -0,0 +1,68 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+import { supabase } from "@/integrations/supabase/client";
+import { useAuth } from "./use-auth";
+
+export interface Profile {
+ id: string;
+ full_name: string;
+ company_name: string | null;
+ cpf_cnpj: string | null;
+ phone: string | null;
+ avatar_url: string | null;
+ stripe_customer_id: string | null;
+ onboarding_completed: boolean;
+}
+
+export function useProfile() {
+ const { user } = useAuth();
+
+ return useQuery({
+ queryKey: ["profile", user?.id],
+ enabled: !!user,
+ queryFn: async (): Promise => {
+ if (!user) return null;
+ const { data, error } = await supabase
+ .from("profiles")
+ .select("*")
+ .eq("id", user.id)
+ .maybeSingle();
+ if (error) throw error;
+ return data as Profile | null;
+ },
+ });
+}
+
+export interface SubscriptionRow {
+ id: string;
+ user_id: string;
+ plan_id: string | null;
+ stripe_subscription_id: string | null;
+ status: "active" | "trialing" | "past_due" | "canceled" | "incomplete" | "incomplete_expired" | "unpaid";
+ billing_cycle: "monthly" | "yearly";
+ current_period_start: string | null;
+ current_period_end: string | null;
+ cancel_at_period_end: boolean;
+}
+
+export function useSubscription() {
+ const { user } = useAuth();
+
+ return useQuery({
+ queryKey: ["subscription", user?.id],
+ enabled: !!user,
+ queryFn: async (): Promise => {
+ if (!user) return null;
+ const { data, error } = await supabase
+ .from("subscriptions")
+ .select("*")
+ .eq("user_id", user.id)
+ .order("created_at", { ascending: false })
+ .limit(1)
+ .maybeSingle();
+ if (error) throw error;
+ return data as SubscriptionRow | null;
+ },
+ });
+}
diff --git a/src/lib/auth-errors.ts b/src/lib/auth-errors.ts
new file mode 100644
index 0000000..27c8c0a
--- /dev/null
+++ b/src/lib/auth-errors.ts
@@ -0,0 +1,26 @@
+/**
+ * Traduz mensagens de erro do Supabase Auth para mensagens em português específicas.
+ */
+export function translateAuthError(message: string | undefined): string {
+ if (!message) return "Algo deu errado. Tente novamente.";
+ const m = message.toLowerCase();
+
+ if (m.includes("invalid login credentials") || m.includes("invalid_credentials"))
+ return "E-mail ou senha incorretos. Tente novamente.";
+ if (m.includes("user already registered") || m.includes("already been registered") || m.includes("already exists"))
+ return "Esse e-mail já possui cadastro. Deseja fazer login?";
+ if (m.includes("password") && (m.includes("short") || m.includes("weak") || m.includes("characters")))
+ return "A senha precisa de pelo menos 8 caracteres, 1 maiúscula e 1 número.";
+ if (m.includes("email") && m.includes("invalid"))
+ return "Formato de e-mail inválido.";
+ if (m.includes("rate limit") || m.includes("too many requests"))
+ return "Muitas tentativas. Aguarde 1 minuto e tente novamente.";
+ if (m.includes("expired") || m.includes("jwt") || m.includes("session"))
+ return "Sua sessão expirou. Faça login novamente.";
+ if (m.includes("email not confirmed"))
+ return "Confirme seu e-mail antes de entrar. Verifique sua caixa de entrada.";
+ if (m.includes("pwned") || m.includes("compromised") || m.includes("breach"))
+ return "Esta senha apareceu em vazamentos públicos. Escolha outra mais forte.";
+
+ return message;
+}
diff --git a/src/lib/password.ts b/src/lib/password.ts
new file mode 100644
index 0000000..58ffa5f
--- /dev/null
+++ b/src/lib/password.ts
@@ -0,0 +1,14 @@
+export type PasswordStrength = 0 | 1 | 2 | 3;
+
+export function passwordStrength(password: string): PasswordStrength {
+ let score = 0;
+ if (password.length >= 8) score++;
+ if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++;
+ if (/\d/.test(password) && /[^A-Za-z0-9]/.test(password)) score++;
+ if (password.length >= 12) score = Math.min(3, score + 1) as PasswordStrength;
+ return Math.min(3, score) as PasswordStrength;
+}
+
+export function passwordStrengthLabel(s: PasswordStrength): string {
+ return ["Muito fraca", "Fraca", "Média", "Forte"][s];
+}