Browse Source

feat: migrate registration to Supabase Auth SDK and add Settings page #106

- Replace custom email verification flow with Supabase Auth SDK
- Users now receive Supabase's built-in email confirmation
- Update Signup.tsx to use supabase.auth.signUp()
- Update Login.tsx to use supabase.auth.signInWithPassword()
- Update AuthContext to use Supabase Auth SDK for login/logout
- Create new Settings page (/settings) for user account management:
  - Update profile (full name, company name)
  - Change email (with confirmation)
  - Change password (with validation)
- Add Supabase client configuration in lib/supabase.ts
- Install @supabase/supabase-js dependency
- Add success/error message display on Login page
- All password changes follow Supabase's security requirements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 4 months ago
parent
commit
e9ba22fc18

+ 1 - 0
shopcall.ai-main/package.json

@@ -39,6 +39,7 @@
     "@radix-ui/react-toggle": "^1.1.0",
     "@radix-ui/react-toggle-group": "^1.1.0",
     "@radix-ui/react-tooltip": "^1.1.4",
+    "@supabase/supabase-js": "^2.84.0",
     "@tanstack/react-query": "^5.56.2",
     "class-variance-authority": "^0.7.1",
     "clsx": "^2.1.1",

+ 2 - 0
shopcall.ai-main/src/App.tsx

@@ -17,6 +17,7 @@ import AIConfig from "./pages/AIConfig";
 import ManageStoreData from "./pages/ManageStoreData";
 import APIKeys from "./pages/APIKeys";
 import Onboarding from "./pages/Onboarding";
+import Settings from "./pages/Settings";
 import About from "./pages/About";
 import Privacy from "./pages/Privacy";
 import Terms from "./pages/Terms";
@@ -51,6 +52,7 @@ const App = () => (
               <Route path="/manage-store-data" element={<ManageStoreData />} />
               <Route path="/api-keys" element={<APIKeys />} />
               <Route path="/onboarding" element={<Onboarding />} />
+              <Route path="/settings" element={<Settings />} />
             </Route>
             <Route path="/about" element={<About />} />
             <Route path="/privacy" element={<Privacy />} />

+ 24 - 12
shopcall.ai-main/src/components/context/AuthContext.tsx

@@ -1,8 +1,9 @@
 import { createContext, useContext, useState, ReactNode } from 'react';
 import { useEffect } from 'react';
 import { useNavigate } from "react-router-dom";
+import { supabase } from '@/lib/supabase';
 
-// Get API URL from environment variables
+// Get API URL from environment variables (for backward compatibility)
 const API_URL = import.meta.env.VITE_API_URL || 'https://ztklqodcdjeqpsvhlpud.supabase.co/functions/v1';
 
 interface User {
@@ -159,16 +160,23 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
     }
 
     const login = async (user_login_data: User) => {
-        const response = await fetch(`${API_URL}/auth/login`, {
-            method: "POST",
-            headers: {
-                "Content-Type": "application/json",
-            },
-            body: JSON.stringify(user_login_data),
+        // Use Supabase Auth SDK for login
+        const { data, error } = await supabase.auth.signInWithPassword({
+            email: user_login_data.email,
+            password: user_login_data.password,
         });
-        const login_response = await response.json();
-        if (login_response.success) {
-            // setIsAuthenticated(true);
+
+        if (error) {
+            throw new Error(error.message);
+        }
+
+        if (data.session) {
+            // Store session data in the format expected by existing code
+            const login_response = {
+                success: true,
+                user: data.user,
+                session: data.session
+            };
             localStorage.setItem("session_data", JSON.stringify(login_response));
 
             // Check for pending integration redirect
@@ -180,11 +188,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
                 check_auth("/dashboard");
             }
         } else {
-            throw new Error(login_response.error);
+            throw new Error("Login failed - no session created");
         }
     };
 
-    const logout = () => {
+    const logout = async () => {
+        // Sign out from Supabase
+        await supabase.auth.signOut();
+
+        // Clear local storage
         localStorage.removeItem("session_data");
         setIsAuthenticated(false);
         localStorage.setItem('IsAuthenticated', 'false');

+ 10 - 0
shopcall.ai-main/src/lib/supabase.ts

@@ -0,0 +1,10 @@
+import { createClient } from '@supabase/supabase-js'
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
+const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
+
+if (!supabaseUrl || !supabaseAnonKey) {
+  throw new Error('Missing Supabase environment variables. Please check your .env file.')
+}
+
+export const supabase = createClient(supabaseUrl, supabaseAnonKey)

+ 37 - 7
shopcall.ai-main/src/pages/Login.tsx

@@ -1,10 +1,10 @@
 
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Phone, Mail, Lock, Loader2 } from "lucide-react";
-import { useNavigate } from "react-router-dom";
+import { Phone, Mail, Lock, Loader2, CheckCircle, AlertCircle } from "lucide-react";
+import { useNavigate, useLocation } from "react-router-dom";
 import { useAuth } from "@/components/context/AuthContext";
 import { useTranslation } from 'react-i18next';
 import { LanguageSelector } from "@/components/LanguageSelector";
@@ -12,21 +12,37 @@ import { LanguageSelector } from "@/components/LanguageSelector";
 const Login = () => {
   const { t } = useTranslation();
   const { login } = useAuth();
+  const location = useLocation();
   const [email, setEmail] = useState("");
   const [password, setPassword] = useState("");
   const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState("");
+  const [successMessage, setSuccessMessage] = useState("");
   const navigate = useNavigate();
 
+  // Check for success message from signup
+  useEffect(() => {
+    if (location.state?.message) {
+      setSuccessMessage(location.state.message);
+      if (location.state?.email) {
+        setEmail(location.state.email);
+      }
+      // Clear the state
+      navigate(location.pathname, { replace: true, state: {} });
+    }
+  }, [location, navigate]);
+
   const handleLogin = async (e: React.FormEvent) => {
     e.preventDefault();
     setIsLoading(true);
-    
+    setError("");
+    setSuccessMessage("");
+
     try {
-      // For now, just redirect to dashboard after login
-      // In a real app, this would handle authentication
       await login({ email: email, password: password });
-    } catch (error) {
+    } catch (error: any) {
       console.error("Login failed:", error);
+      setError(error.message || t('auth.login.error') || 'Login failed. Please check your credentials.');
     } finally {
       setIsLoading(false);
     }
@@ -55,6 +71,20 @@ const Login = () => {
           </CardHeader>
           <CardContent>
             <form onSubmit={handleLogin} className="space-y-4">
+              {successMessage && (
+                <div className="bg-green-900/20 border border-green-500/50 rounded-md p-3 flex items-center gap-2">
+                  <CheckCircle className="w-4 h-4 text-green-400 flex-shrink-0" />
+                  <span className="text-green-400 text-sm">{successMessage}</span>
+                </div>
+              )}
+
+              {error && (
+                <div className="bg-red-900/20 border border-red-500/50 rounded-md p-3 flex items-center gap-2">
+                  <AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
+                  <span className="text-red-400 text-sm">{error}</span>
+                </div>
+              )}
+
               <div className="space-y-2">
                 <label className="text-sm font-medium text-slate-300">{t('auth.login.email')}</label>
                 <div className="relative">

+ 371 - 0
shopcall.ai-main/src/pages/Settings.tsx

@@ -0,0 +1,371 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Mail, Lock, Loader2, AlertCircle, CheckCircle, User, Building } from "lucide-react";
+import { supabase } from "@/lib/supabase";
+import { useTranslation } from "react-i18next";
+
+const Settings = () => {
+  const { t } = useTranslation();
+  const [isLoading, setIsLoading] = useState(false);
+  const [user, setUser] = useState<any>(null);
+  const [profile, setProfile] = useState<any>(null);
+
+  // Form states
+  const [fullName, setFullName] = useState("");
+  const [companyName, setCompanyName] = useState("");
+  const [currentEmail, setCurrentEmail] = useState("");
+  const [newEmail, setNewEmail] = useState("");
+  const [currentPassword, setCurrentPassword] = useState("");
+  const [newPassword, setNewPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+
+  // Messages
+  const [successMessage, setSuccessMessage] = useState("");
+  const [errorMessage, setErrorMessage] = useState("");
+
+  useEffect(() => {
+    loadUserData();
+  }, []);
+
+  const loadUserData = async () => {
+    try {
+      const { data: { user } } = await supabase.auth.getUser();
+      if (user) {
+        setUser(user);
+        setCurrentEmail(user.email || "");
+        setFullName(user.user_metadata?.full_name || "");
+        setCompanyName(user.user_metadata?.company_name || "");
+
+        // Load profile data
+        const { data: profileData } = await supabase
+          .from('profiles')
+          .select('*')
+          .eq('id', user.id)
+          .single();
+
+        if (profileData) {
+          setProfile(profileData);
+          setFullName(profileData.full_name || user.user_metadata?.full_name || "");
+          setCompanyName(profileData.company_name || user.user_metadata?.company_name || "");
+        }
+      }
+    } catch (error) {
+      console.error("Error loading user data:", error);
+    }
+  };
+
+  const handleUpdateProfile = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsLoading(true);
+    setErrorMessage("");
+    setSuccessMessage("");
+
+    try {
+      const { data: { user } } = await supabase.auth.getUser();
+      if (!user) {
+        throw new Error("Not authenticated");
+      }
+
+      // Update user metadata
+      const { error: metadataError } = await supabase.auth.updateUser({
+        data: {
+          full_name: fullName,
+          company_name: companyName,
+        }
+      });
+
+      if (metadataError) throw metadataError;
+
+      // Update profile table
+      const { error: profileError } = await supabase
+        .from('profiles')
+        .upsert({
+          id: user.id,
+          full_name: fullName,
+          company_name: companyName,
+          email: user.email,
+          updated_at: new Date().toISOString()
+        });
+
+      if (profileError) throw profileError;
+
+      setSuccessMessage(t('settings.profileUpdated') || "Profile updated successfully!");
+      await loadUserData();
+    } catch (error: any) {
+      console.error("Error updating profile:", error);
+      setErrorMessage(error.message || "Failed to update profile");
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleUpdateEmail = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsLoading(true);
+    setErrorMessage("");
+    setSuccessMessage("");
+
+    try {
+      if (!newEmail || !newEmail.includes("@")) {
+        throw new Error("Please enter a valid email address");
+      }
+
+      const { error } = await supabase.auth.updateUser({
+        email: newEmail
+      });
+
+      if (error) throw error;
+
+      setSuccessMessage(
+        t('settings.emailUpdateSent') ||
+        "Confirmation email sent! Please check both your old and new email addresses to confirm the change."
+      );
+      setNewEmail("");
+    } catch (error: any) {
+      console.error("Error updating email:", error);
+      setErrorMessage(error.message || "Failed to update email");
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleUpdatePassword = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsLoading(true);
+    setErrorMessage("");
+    setSuccessMessage("");
+
+    try {
+      // Validate password requirements
+      if (!newPassword || newPassword.length < 8) {
+        throw new Error("Password must be at least 8 characters long");
+      }
+
+      if (newPassword !== confirmPassword) {
+        throw new Error("Passwords do not match");
+      }
+
+      const { error } = await supabase.auth.updateUser({
+        password: newPassword
+      });
+
+      if (error) throw error;
+
+      setSuccessMessage(t('settings.passwordUpdated') || "Password updated successfully!");
+      setCurrentPassword("");
+      setNewPassword("");
+      setConfirmPassword("");
+    } catch (error: any) {
+      console.error("Error updating password:", error);
+      setErrorMessage(error.message || "Failed to update password");
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-900 text-white p-6">
+      <div className="max-w-4xl mx-auto space-y-6">
+        <div>
+          <h1 className="text-3xl font-bold mb-2">{t('settings.title') || 'Account Settings'}</h1>
+          <p className="text-slate-400">{t('settings.subtitle') || 'Manage your account details and preferences'}</p>
+        </div>
+
+        {/* Success/Error Messages */}
+        {successMessage && (
+          <div className="bg-green-900/20 border border-green-500/50 rounded-md p-3 flex items-center gap-2">
+            <CheckCircle className="w-4 h-4 text-green-400 flex-shrink-0" />
+            <span className="text-green-400 text-sm">{successMessage}</span>
+          </div>
+        )}
+
+        {errorMessage && (
+          <div className="bg-red-900/20 border border-red-500/50 rounded-md p-3 flex items-center gap-2">
+            <AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
+            <span className="text-red-400 text-sm">{errorMessage}</span>
+          </div>
+        )}
+
+        {/* Profile Information */}
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <CardTitle className="text-white">{t('settings.profileInfo') || 'Profile Information'}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleUpdateProfile} className="space-y-4">
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.fullName') || 'Full Name'}
+                </label>
+                <div className="relative">
+                  <User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="text"
+                    placeholder="John Doe"
+                    value={fullName}
+                    onChange={(e) => setFullName(e.target.value)}
+                    className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                  />
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.companyName') || 'Company Name'}
+                </label>
+                <div className="relative">
+                  <Building className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="text"
+                    placeholder="Acme Inc."
+                    value={companyName}
+                    onChange={(e) => setCompanyName(e.target.value)}
+                    className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                  />
+                </div>
+              </div>
+
+              <Button
+                type="submit"
+                className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
+                disabled={isLoading}
+              >
+                {isLoading ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    {t('settings.updating') || 'Updating...'}
+                  </>
+                ) : (
+                  t('settings.updateProfile') || 'Update Profile'
+                )}
+              </Button>
+            </form>
+          </CardContent>
+        </Card>
+
+        {/* Email Change */}
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <CardTitle className="text-white">{t('settings.changeEmail') || 'Change Email'}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleUpdateEmail} className="space-y-4">
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.currentEmail') || 'Current Email'}
+                </label>
+                <div className="relative">
+                  <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="email"
+                    value={currentEmail}
+                    className="pl-10 bg-slate-700 border-slate-600 text-slate-500 placeholder-slate-400"
+                    disabled
+                  />
+                </div>
+              </div>
+
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.newEmail') || 'New Email'}
+                </label>
+                <div className="relative">
+                  <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="email"
+                    placeholder="newemail@example.com"
+                    value={newEmail}
+                    onChange={(e) => setNewEmail(e.target.value)}
+                    className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                    required
+                  />
+                </div>
+              </div>
+
+              <Button
+                type="submit"
+                className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
+                disabled={isLoading || !newEmail}
+              >
+                {isLoading ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    {t('settings.updating') || 'Updating...'}
+                  </>
+                ) : (
+                  t('settings.updateEmail') || 'Update Email'
+                )}
+              </Button>
+            </form>
+          </CardContent>
+        </Card>
+
+        {/* Password Change */}
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <CardTitle className="text-white">{t('settings.changePassword') || 'Change Password'}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleUpdatePassword} className="space-y-4">
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.newPassword') || 'New Password'}
+                </label>
+                <div className="relative">
+                  <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="password"
+                    placeholder="Enter new password"
+                    value={newPassword}
+                    onChange={(e) => setNewPassword(e.target.value)}
+                    className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                    required
+                  />
+                </div>
+                <p className="text-xs text-slate-400">
+                  {t('settings.passwordRequirement') || 'Password must be at least 8 characters'}
+                </p>
+              </div>
+
+              <div className="space-y-2">
+                <label className="text-sm font-medium text-slate-300">
+                  {t('settings.confirmPassword') || 'Confirm Password'}
+                </label>
+                <div className="relative">
+                  <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                  <Input
+                    type="password"
+                    placeholder="Confirm new password"
+                    value={confirmPassword}
+                    onChange={(e) => setConfirmPassword(e.target.value)}
+                    className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                    required
+                  />
+                </div>
+              </div>
+
+              <Button
+                type="submit"
+                className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
+                disabled={isLoading || !newPassword || !confirmPassword}
+              >
+                {isLoading ? (
+                  <>
+                    <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                    {t('settings.updating') || 'Updating...'}
+                  </>
+                ) : (
+                  t('settings.updatePassword') || 'Update Password'
+                )}
+              </Button>
+            </form>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+};
+
+export default Settings;

+ 45 - 51
shopcall.ai-main/src/pages/Signup.tsx

@@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Phone, Mail, Lock, User, Loader2, AlertCircle } from "lucide-react";
 import { useLocation, useNavigate } from "react-router-dom";
-import { API_URL } from "@/lib/config";
+import { supabase } from "@/lib/supabase";
 import { useTranslation } from "react-i18next";
 
 const Signup = () => {
@@ -69,69 +69,63 @@ const Signup = () => {
     return Object.keys(newErrors).length === 0;
   };
 
-  const handleSignup = (e: React.FormEvent) => {
+  const handleSignup = async (e: React.FormEvent) => {
     e.preventDefault();
-    
+
     // Clear previous errors
     setErrors({});
-    
+
     // Validate form
     if (!validateForm()) {
       return;
     }
-    
-    const handleSignup = async () => {
-      setIsLoading(true);
-      try {
-        const response = await fetch(`${API_URL}/auth/signup`, {
-          method: "POST",
-          headers: {
-            "Content-Type": "application/json",
+
+    setIsLoading(true);
+    try {
+      // Use Supabase Auth SDK to sign up with email confirmation
+      const { data, error } = await supabase.auth.signUp({
+        email: email,
+        password: password,
+        options: {
+          data: {
+            full_name: name,
+            company_name: company,
+            user_name: name.toLowerCase().replace(/\s+/g, "_"),
           },
-          body: JSON.stringify({ 
-            email: email, 
-            password: password, 
-            full_name: name, 
-            company_name: company, 
-            user_name: name.toLowerCase().replace(/\s+/g, "_") 
-          }),
-        });
-        
-        if (!response.ok) {
-          const errorData = await response.json().catch(() => ({}));
-          throw new Error(errorData.message || "Signup failed");
+          emailRedirectTo: `${window.location.origin}/login`
         }
-        
-        const data = await response.json();        
-        // Navigate to OTP page with email verification state
-        navigate("/otp", { 
-          state: { 
-            fromSignup: true, 
-            email: email,
-            signupId: data.signupId,
-            fromEmailVerification: true,
-            form_data: {
-              email: email,
-              password: password,
-              full_name: name,
-              company_name: company,
-              user_name: name.toLowerCase().replace(/\s+/g, "_")
-            }
-          },
-          replace: true 
-        });
-        
-      } catch (error) {
-        console.error("Signup error:", error);
+      });
+
+      if (error) {
+        throw error;
+      }
+
+      // Check if email confirmation is required
+      if (data.user && data.user.identities && data.user.identities.length === 0) {
+        // User already exists
         setErrors({
-          general: error instanceof Error ? error.message : t('signup.errors.signupFailed')
+          general: t('signup.errors.emailAlreadyRegistered') || 'This email is already registered. Please login instead.'
         });
-      } finally {
-        setIsLoading(false);
+        return;
       }
-    };
 
-    handleSignup();
+      // Successfully signed up - show success message
+      navigate("/login", {
+        state: {
+          message: t('signup.successMessage') || 'Account created successfully! Please check your email to verify your account before logging in.',
+          email: email
+        },
+        replace: true
+      });
+
+    } catch (error: any) {
+      console.error("Signup error:", error);
+      setErrors({
+        general: error.message || t('signup.errors.signupFailed') || 'Registration failed. Please try again.'
+      });
+    } finally {
+      setIsLoading(false);
+    }
   };
 
   return (