Browse Source

fix: multiple improvements for store handling and scraper API

- Fix ShopContext to fetch stores on mount instead of relying on AppSidebar
- Fix infinite loading on page refresh for store-protected routes
- Add StoreProtectedRoute component for routes requiring active store
- Fix duplicate /api/api in scraper API endpoint URL
- Fix PDF preview URL to use configured VITE_SUPABASE_URL
- Update scraper-client with new API response format (pagination, title)
- Refactor Settings page into SettingsContent component
- Fix ShopRenter uninstall webhook HMAC validation
- Add translations for store protection messages

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

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

+ 11 - 5
shopcall.ai-main/src/App.tsx

@@ -8,6 +8,7 @@ import { BrowserRouter, Routes, Route } from "react-router-dom";
 import { AuthProvider } from "./components/context/AuthContext";
 import { AuthProvider } from "./components/context/AuthContext";
 import { ShopProvider } from "./components/context/ShopContext";
 import { ShopProvider } from "./components/context/ShopContext";
 import PrivateRoute from "./components/PrivateRoute";
 import PrivateRoute from "./components/PrivateRoute";
+import StoreProtectedRoute from "./components/StoreProtectedRoute";
 import { Loader2 } from "lucide-react";
 import { Loader2 } from "lucide-react";
 
 
 // Lazy load page components for code splitting
 // Lazy load page components for code splitting
@@ -69,19 +70,24 @@ const App = () => (
               {/* Lazy-loaded routes */}
               {/* Lazy-loaded routes */}
               <Route path="/otp" element={<OTP />} />
               <Route path="/otp" element={<OTP />} />
               <Route element={<PrivateRoute />}>
               <Route element={<PrivateRoute />}>
+                {/* Pages that don't require an active store */}
                 <Route path="/dashboard" element={<Dashboard />} />
                 <Route path="/dashboard" element={<Dashboard />} />
                 <Route path="/call-logs" element={<CallLogs />} />
                 <Route path="/call-logs" element={<CallLogs />} />
                 <Route path="/analytics" element={<Analytics />} />
                 <Route path="/analytics" element={<Analytics />} />
                 <Route path="/webshops" element={<Webshops />} />
                 <Route path="/webshops" element={<Webshops />} />
                 <Route path="/phone-numbers" element={<PhoneNumbers />} />
                 <Route path="/phone-numbers" element={<PhoneNumbers />} />
-                <Route path="/ai-config" element={<AIConfig />} />
-                <Route path="/products" element={<Products />} />
-                <Route path="/website-content" element={<WebsiteContent />} />
-                <Route path="/custom-content" element={<CustomContent />} />
-                <Route path="/manage-store-data" element={<ManageStoreData />} />
                 <Route path="/api-keys" element={<APIKeys />} />
                 <Route path="/api-keys" element={<APIKeys />} />
                 <Route path="/onboarding" element={<Onboarding />} />
                 <Route path="/onboarding" element={<Onboarding />} />
                 <Route path="/settings" element={<Settings />} />
                 <Route path="/settings" element={<Settings />} />
+
+                {/* Pages that require an active store */}
+                <Route element={<StoreProtectedRoute />}>
+                  <Route path="/ai-config" element={<AIConfig />} />
+                  <Route path="/products" element={<Products />} />
+                  <Route path="/website-content" element={<WebsiteContent />} />
+                  <Route path="/custom-content" element={<CustomContent />} />
+                  <Route path="/manage-store-data" element={<ManageStoreData />} />
+                </Route>
               </Route>
               </Route>
               <Route path="/about" element={<About />} />
               <Route path="/about" element={<About />} />
               <Route path="/privacy" element={<Privacy />} />
               <Route path="/privacy" element={<Privacy />} />

+ 39 - 13
shopcall.ai-main/src/components/AppSidebar.tsx

@@ -100,6 +100,11 @@ export function AppSidebar() {
   ];
   ];
 
 
   const configItems = [
   const configItems = [
+    {
+      title: t('sidebar.settings'),
+      icon: Settings,
+      url: "/settings",
+    },
     {
     {
       title: "Billing & Plan",
       title: "Billing & Plan",
       icon: CreditCard,
       icon: CreditCard,
@@ -109,7 +114,8 @@ export function AppSidebar() {
 
 
   const handleShopChange = (shopId: string) => {
   const handleShopChange = (shopId: string) => {
     const shop = stores.find(s => s.id === shopId);
     const shop = stores.find(s => s.id === shopId);
-    if (shop) {
+    // Prevent selecting inactive stores
+    if (shop && shop.is_active !== false) {
       setSelectedShop(shop);
       setSelectedShop(shop);
 
 
       // Update URL query parameter if the current page uses the shop parameter
       // Update URL query parameter if the current page uses the shop parameter
@@ -143,14 +149,29 @@ export function AppSidebar() {
                 </div>
                 </div>
               </SelectTrigger>
               </SelectTrigger>
               <SelectContent className="bg-slate-800 border-slate-600">
               <SelectContent className="bg-slate-800 border-slate-600">
-                {stores.map((store) => (
-                  <SelectItem key={store.id} value={store.id} className="text-white hover:bg-slate-700">
-                    <div className="flex flex-col items-start">
-                      <div className="font-medium">{store.store_name || 'Unnamed Store'}</div>
-                      <div className="text-xs text-slate-400 capitalize">{store.platform_name}</div>
-                    </div>
-                  </SelectItem>
-                ))}
+                {stores.map((store) => {
+                  const isInactive = store.is_active === false;
+                  return (
+                    <SelectItem
+                      key={store.id}
+                      value={store.id}
+                      disabled={isInactive}
+                      className={`text-white hover:bg-slate-700 ${isInactive ? 'opacity-50 cursor-not-allowed' : ''}`}
+                    >
+                      <div className="flex flex-col items-start">
+                        <div className="flex items-center gap-2">
+                          <span className="font-medium">{store.store_name || 'Unnamed Store'}</span>
+                          {isInactive && (
+                            <span className="text-[10px] bg-red-500/20 text-red-400 px-1.5 py-0.5 rounded">
+                              {t('sidebar.inactive') || 'Inactive'}
+                            </span>
+                          )}
+                        </div>
+                        <div className="text-xs text-slate-400 capitalize">{store.platform_name}</div>
+                      </div>
+                    </SelectItem>
+                  );
+                })}
               </SelectContent>
               </SelectContent>
             </Select>
             </Select>
           </div>
           </div>
@@ -324,10 +345,15 @@ export function AppSidebar() {
           <div className="flex justify-center mb-2">
           <div className="flex justify-center mb-2">
             <LanguageSelector />
             <LanguageSelector />
           </div>
           </div>
-          <div className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-lg">
-            <Settings className="w-4 h-4 text-slate-400" />
-            <span className="text-sm text-slate-300">{t('sidebar.settings')}</span>
-          </div>
+          <button
+            onClick={() => navigate('/settings')}
+            className={`flex items-center gap-3 p-3 w-full rounded-lg hover:bg-slate-800 transition-colors cursor-pointer ${
+              currentPath === '/settings' ? 'bg-cyan-500/20 text-cyan-400' : 'bg-slate-800/50'
+            }`}
+          >
+            <Settings className={`w-4 h-4 ${currentPath === '/settings' ? 'text-cyan-400' : 'text-slate-400'}`} />
+            <span className={`text-sm ${currentPath === '/settings' ? 'text-cyan-400' : 'text-slate-300'}`}>{t('sidebar.settings')}</span>
+          </button>
           <button
           <button
             onClick={logout}
             onClick={logout}
             className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-lg hover:bg-slate-800 transition-colors cursor-pointer"
             className="flex items-center gap-3 p-3 bg-slate-800/50 rounded-lg hover:bg-slate-800 transition-colors cursor-pointer"

+ 29 - 5
shopcall.ai-main/src/components/CustomContentViewer.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
 import {
 import {
   Dialog,
   Dialog,
   DialogContent,
   DialogContent,
@@ -12,6 +12,25 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
 import { supabase } from "@/lib/supabase";
 import { supabase } from "@/lib/supabase";
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+// Helper to fix Supabase storage URLs to use the correct base URL from environment
+const fixSupabaseStorageUrl = (url: string | undefined): string | undefined => {
+  if (!url) return url;
+
+  const configuredSupabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+  if (!configuredSupabaseUrl) return url;
+
+  // Match any Supabase storage URL pattern and replace with configured URL
+  // Pattern: https://<project-ref>.supabase.co/storage/v1/...
+  const supabaseStoragePattern = /^https:\/\/[a-z0-9]+\.supabase\.co(\/storage\/v1\/.*)$/;
+  const match = url.match(supabaseStoragePattern);
+
+  if (match) {
+    return `${configuredSupabaseUrl}${match[1]}`;
+  }
+
+  return url;
+};
+
 interface CustomContentViewerProps {
 interface CustomContentViewerProps {
   contentId: string;
   contentId: string;
   storeId: string;
   storeId: string;
@@ -86,9 +105,14 @@ export function CustomContentViewer({
     }
     }
   };
   };
 
 
+  // Get the corrected PDF URL using the configured Supabase URL
+  const correctedPdfUrl = useMemo(() => {
+    return fixSupabaseStorageUrl(content?.pdfUrl);
+  }, [content?.pdfUrl]);
+
   const handleOpenPdfInNewTab = () => {
   const handleOpenPdfInNewTab = () => {
-    if (content?.pdfUrl) {
-      window.open(content.pdfUrl, '_blank');
+    if (correctedPdfUrl) {
+      window.open(correctedPdfUrl, '_blank');
     }
     }
   };
   };
 
 
@@ -135,7 +159,7 @@ export function CustomContentViewer({
               </div>
               </div>
 
 
               {/* Content Viewer */}
               {/* Content Viewer */}
-              {content.contentType === "pdf_upload" && content.pdfUrl ? (
+              {content.contentType === "pdf_upload" && correctedPdfUrl ? (
                 <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
                 <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
                   <TabsList className="bg-slate-700 border border-slate-600">
                   <TabsList className="bg-slate-700 border border-slate-600">
                     <TabsTrigger
                     <TabsTrigger
@@ -169,7 +193,7 @@ export function CustomContentViewer({
                       </div>
                       </div>
                       <div className="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
                       <div className="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
                         <iframe
                         <iframe
-                          src={content.pdfUrl}
+                          src={correctedPdfUrl}
                           className="w-full h-[600px]"
                           className="w-full h-[600px]"
                           title={content.title}
                           title={content.title}
                         />
                         />

+ 388 - 0
shopcall.ai-main/src/components/SettingsContent.tsx

@@ -0,0 +1,388 @@
+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 as UserIcon, Building } from "lucide-react";
+import { supabase } from "@/lib/supabase";
+import { useTranslation } from "react-i18next";
+
+interface User {
+  id: string;
+  email?: string;
+  user_metadata?: {
+    full_name?: string;
+    company_name?: string;
+    [key: string]: unknown;
+  };
+  [key: string]: unknown;
+}
+
+interface Profile {
+  id: string;
+  full_name: string | null;
+  username: string | null;
+  email: string | null;
+  company_name: string | null;
+  is_verified: boolean | null;
+  created_at: string;
+  updated_at: string;
+}
+
+export function SettingsContent() {
+  const { t } = useTranslation();
+  const [isLoading, setIsLoading] = useState(false);
+  const [user, setUser] = useState<User | null>(null);
+  const [profile, setProfile] = useState<Profile | null>(null);
+
+  // Form states
+  const [fullName, setFullName] = useState("");
+  const [companyName, setCompanyName] = useState("");
+  const [currentEmail, setCurrentEmail] = useState("");
+  const [newEmail, setNewEmail] = 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(t('settings.errors.notAuthenticated'));
+      }
+
+      // 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'));
+      await loadUserData();
+    } catch (error: unknown) {
+      console.error("Error updating profile:", error);
+      setErrorMessage((error as Error)?.message || t('settings.errors.updateProfileFailed'));
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleUpdateEmail = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsLoading(true);
+    setErrorMessage("");
+    setSuccessMessage("");
+
+    try {
+      if (!newEmail || !newEmail.includes("@")) {
+        throw new Error(t('settings.errors.invalidEmail'));
+      }
+
+      const { error } = await supabase.auth.updateUser({
+        email: newEmail
+      });
+
+      if (error) throw error;
+
+      setSuccessMessage(t('settings.emailUpdateSent'));
+      setNewEmail("");
+    } catch (error: unknown) {
+      console.error("Error updating email:", error);
+      setErrorMessage((error as Error)?.message || t('settings.errors.updateEmailFailed'));
+    } 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(t('settings.errors.passwordTooShort'));
+      }
+
+      if (newPassword !== confirmPassword) {
+        throw new Error(t('settings.errors.passwordsDoNotMatch'));
+      }
+
+      const { error } = await supabase.auth.updateUser({
+        password: newPassword
+      });
+
+      if (error) throw error;
+
+      setSuccessMessage(t('settings.passwordUpdated'));
+      setNewPassword("");
+      setConfirmPassword("");
+    } catch (error: unknown) {
+      console.error("Error updating password:", error);
+      setErrorMessage((error as Error)?.message || t('settings.errors.updatePasswordFailed'));
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <main className="flex-1 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-auto">
+      <div className="p-6">
+        <div className="max-w-4xl mx-auto space-y-6">
+          <div>
+            <h1 className="text-3xl font-bold mb-2 text-white">{t('settings.title')}</h1>
+            <p className="text-slate-400">{t('settings.subtitle')}</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')}</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')}
+                  </label>
+                  <div className="relative">
+                    <UserIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                    <Input
+                      type="text"
+                      placeholder={t('settings.placeholders.fullName')}
+                      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')}
+                  </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={t('settings.placeholders.companyName')}
+                      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')}
+                    </>
+                  ) : (
+                    t('settings.updateProfile')
+                  )}
+                </Button>
+              </form>
+            </CardContent>
+          </Card>
+
+          {/* Email Change */}
+          <Card className="bg-slate-800 border-slate-700">
+            <CardHeader>
+              <CardTitle className="text-white">{t('settings.changeEmail')}</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')}
+                  </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')}
+                  </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={t('settings.placeholders.newEmail')}
+                      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')}
+                    </>
+                  ) : (
+                    t('settings.updateEmail')
+                  )}
+                </Button>
+              </form>
+            </CardContent>
+          </Card>
+
+          {/* Password Change */}
+          <Card className="bg-slate-800 border-slate-700">
+            <CardHeader>
+              <CardTitle className="text-white">{t('settings.changePassword')}</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')}
+                  </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={t('settings.placeholders.newPassword')}
+                      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')}
+                  </p>
+                </div>
+
+                <div className="space-y-2">
+                  <label className="text-sm font-medium text-slate-300">
+                    {t('settings.confirmPassword')}
+                  </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={t('settings.placeholders.confirmPassword')}
+                      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')}
+                    </>
+                  ) : (
+                    t('settings.updatePassword')
+                  )}
+                </Button>
+              </form>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    </main>
+  );
+}

+ 152 - 0
shopcall.ai-main/src/components/StoreProtectedRoute.tsx

@@ -0,0 +1,152 @@
+import { Navigate, Outlet, useLocation } from 'react-router-dom';
+import { useShop } from './context/ShopContext';
+import { AlertCircle, Store } from "lucide-react";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { useTranslation } from "react-i18next";
+
+/**
+ * StoreProtectedRoute - Protects routes that require an active store to be selected
+ *
+ * This component checks:
+ * 1. If a store is selected
+ * 2. If the selected store is active (not uninstalled/disabled)
+ *
+ * If no active store is selected, it shows an error message with a link to the webshops page.
+ */
+const StoreProtectedRoute = () => {
+  const { selectedShop, stores, isLoading, storesLoaded } = useShop();
+  const { t } = useTranslation();
+  const location = useLocation();
+
+  // Wait for stores to load - show loading until stores are actually fetched
+  if (isLoading || !storesLoaded) {
+    return (
+      <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center px-4">
+        <div className="w-full max-w-md text-center">
+          <div className="relative mx-auto w-16 h-16 mb-6">
+            <div className="absolute inset-0 rounded-full border-4 border-[#52b3d0]/20"></div>
+            <div className="absolute inset-0 rounded-full border-4 border-transparent border-t-[#52b3d0] animate-spin"></div>
+            <div className="absolute inset-2 rounded-full bg-[#52b3d0]/10 flex items-center justify-center">
+              <Store className="w-6 h-6 text-[#52b3d0]" />
+            </div>
+          </div>
+          <p className="text-slate-400">{t('storeProtectedRoute.loadingStores') || 'Loading stores...'}</p>
+        </div>
+      </div>
+    );
+  }
+
+  // Check if no stores exist at all
+  if (stores.length === 0) {
+    return (
+      <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center px-4">
+        <div className="w-full max-w-md">
+          <Card className="bg-slate-800/50 border-slate-700 backdrop-blur-sm">
+            <CardContent className="p-8">
+              <div className="text-center space-y-6">
+                <div className="mx-auto w-16 h-16 rounded-full bg-amber-500/10 flex items-center justify-center">
+                  <Store className="w-8 h-8 text-amber-500" />
+                </div>
+                <div className="space-y-2">
+                  <h2 className="text-xl font-semibold text-white">
+                    {t('storeProtectedRoute.noStoresTitle') || 'No Stores Connected'}
+                  </h2>
+                  <p className="text-slate-400 text-sm">
+                    {t('storeProtectedRoute.noStoresDescription') || 'You need to connect a store to access this feature.'}
+                  </p>
+                </div>
+                <Button
+                  onClick={() => window.location.href = '/webshops'}
+                  className="bg-[#52b3d0] hover:bg-[#52b3d0]/80 text-white"
+                >
+                  <Store className="w-4 h-4 mr-2" />
+                  {t('storeProtectedRoute.connectStore') || 'Connect a Store'}
+                </Button>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    );
+  }
+
+  // Check if no store is selected
+  if (!selectedShop) {
+    return (
+      <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center px-4">
+        <div className="w-full max-w-md">
+          <Card className="bg-slate-800/50 border-slate-700 backdrop-blur-sm">
+            <CardContent className="p-8">
+              <div className="text-center space-y-6">
+                <div className="mx-auto w-16 h-16 rounded-full bg-amber-500/10 flex items-center justify-center">
+                  <AlertCircle className="w-8 h-8 text-amber-500" />
+                </div>
+                <div className="space-y-2">
+                  <h2 className="text-xl font-semibold text-white">
+                    {t('storeProtectedRoute.noStoreSelectedTitle') || 'No Store Selected'}
+                  </h2>
+                  <p className="text-slate-400 text-sm">
+                    {t('storeProtectedRoute.noStoreSelectedDescription') || 'Please select an active store from the sidebar to access this feature.'}
+                  </p>
+                </div>
+                <Button
+                  onClick={() => window.location.href = '/webshops'}
+                  className="bg-[#52b3d0] hover:bg-[#52b3d0]/80 text-white"
+                >
+                  <Store className="w-4 h-4 mr-2" />
+                  {t('storeProtectedRoute.manageStores') || 'Manage Stores'}
+                </Button>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    );
+  }
+
+  // Check if selected store is inactive (uninstalled/disabled)
+  if (selectedShop.is_active === false) {
+    return (
+      <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 flex items-center justify-center px-4">
+        <div className="w-full max-w-md">
+          <Card className="bg-slate-800/50 border-red-700/50 backdrop-blur-sm">
+            <CardContent className="p-8">
+              <div className="text-center space-y-6">
+                <div className="mx-auto w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
+                  <AlertCircle className="w-8 h-8 text-red-500" />
+                </div>
+                <div className="space-y-2">
+                  <h2 className="text-xl font-semibold text-white">
+                    {t('storeProtectedRoute.storeInactiveTitle') || 'Store is Inactive'}
+                  </h2>
+                  <p className="text-slate-400 text-sm">
+                    {t('storeProtectedRoute.storeInactiveDescription') || 'The selected store has been disconnected or disabled. Please select another store or reconnect this one.'}
+                  </p>
+                  <div className="mt-4 p-3 bg-slate-700/50 rounded-lg">
+                    <p className="text-sm text-slate-300 font-medium">{selectedShop.store_name}</p>
+                    <p className="text-xs text-slate-500 capitalize">{selectedShop.platform_name}</p>
+                  </div>
+                </div>
+                <div className="flex flex-col gap-2">
+                  <Button
+                    onClick={() => window.location.href = '/webshops'}
+                    className="bg-[#52b3d0] hover:bg-[#52b3d0]/80 text-white"
+                  >
+                    <Store className="w-4 h-4 mr-2" />
+                    {t('storeProtectedRoute.manageStores') || 'Manage Stores'}
+                  </Button>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    );
+  }
+
+  // All checks passed, render the protected content
+  return <Outlet />;
+};
+
+export default StoreProtectedRoute;

+ 85 - 11
shopcall.ai-main/src/components/context/ShopContext.tsx

@@ -1,10 +1,12 @@
 import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
 import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import { API_URL } from '@/lib/config';
 
 
 interface StoreData {
 interface StoreData {
   id: string;
   id: string;
   store_name: string | null;
   store_name: string | null;
   platform_name: string;
   platform_name: string;
   store_url: string | null;
   store_url: string | null;
+  is_active: boolean | null;
 }
 }
 
 
 interface ShopContextType {
 interface ShopContextType {
@@ -13,32 +15,104 @@ interface ShopContextType {
   stores: StoreData[];
   stores: StoreData[];
   setStores: (stores: StoreData[]) => void;
   setStores: (stores: StoreData[]) => void;
   isLoading: boolean;
   isLoading: boolean;
+  storesLoaded: boolean;
+  refreshStores: () => Promise<void>;
 }
 }
 
 
 const ShopContext = createContext<ShopContextType | undefined>(undefined);
 const ShopContext = createContext<ShopContextType | undefined>(undefined);
 
 
 export const ShopProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
 export const ShopProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
   const [selectedShop, setSelectedShopState] = useState<StoreData | null>(null);
   const [selectedShop, setSelectedShopState] = useState<StoreData | null>(null);
-  const [stores, setStores] = useState<StoreData[]>([]);
+  const [stores, setStoresState] = useState<StoreData[]>([]);
   const [isLoading, setIsLoading] = useState(true);
   const [isLoading, setIsLoading] = useState(true);
+  const [storesLoaded, setStoresLoaded] = useState(false);
 
 
-  // Load selected shop from localStorage on mount
+  // Wrap setStores to track when stores are actually loaded
+  const setStores = (newStores: StoreData[]) => {
+    setStoresState(newStores);
+    setStoresLoaded(true);
+  };
+
+  // Fetch stores function - can be called from context or externally
+  const fetchStores = async () => {
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        // No session - mark as loaded with empty stores
+        setStores([]);
+        return;
+      }
+
+      const session = JSON.parse(sessionData);
+      const response = await fetch(`${API_URL}/api/stores`, {
+        headers: {
+          'Authorization': `Bearer ${session.session.access_token}`,
+        }
+      });
+
+      if (response.ok) {
+        const data = await response.json();
+        setStores(data.stores || []);
+      } else {
+        // Request failed - mark as loaded with empty stores
+        setStores([]);
+      }
+    } catch (error) {
+      console.error('Error fetching stores in ShopContext:', error);
+      // Error occurred - mark as loaded with empty stores
+      setStores([]);
+    }
+  };
+
+  // Fetch stores on mount
   useEffect(() => {
   useEffect(() => {
+    fetchStores();
+  }, []);
+
+  // Load selected shop from URL param or localStorage when stores are loaded
+  useEffect(() => {
+    // Don't process until stores are actually loaded
+    if (!storesLoaded) {
+      return;
+    }
+
+    const activeStores = stores.filter(s => s.is_active !== false);
+
+    // First priority: check URL query parameter for shop ID
+    const urlParams = new URLSearchParams(window.location.search);
+    const urlShopId = urlParams.get('shop');
+
+    if (urlShopId && stores.length > 0) {
+      const shopFromUrl = stores.find(s => s.id === urlShopId);
+      if (shopFromUrl && shopFromUrl.is_active !== false) {
+        setSelectedShopState(shopFromUrl);
+        localStorage.setItem('selected_shop_id', urlShopId);
+        setIsLoading(false);
+        return;
+      }
+    }
+
+    // Second priority: check localStorage
     const savedShopId = localStorage.getItem('selected_shop_id');
     const savedShopId = localStorage.getItem('selected_shop_id');
+
     if (savedShopId && stores.length > 0) {
     if (savedShopId && stores.length > 0) {
       const shop = stores.find(s => s.id === savedShopId);
       const shop = stores.find(s => s.id === savedShopId);
-      if (shop) {
+      // Only auto-select if the saved shop is active
+      if (shop && shop.is_active !== false) {
         setSelectedShopState(shop);
         setSelectedShopState(shop);
-      } else if (stores.length > 0) {
-        // If saved shop not found, select first available
-        setSelectedShopState(stores[0]);
+      } else if (activeStores.length > 0) {
+        // If saved shop not found or inactive, select first active store
+        setSelectedShopState(activeStores[0]);
+      } else {
+        // No active stores available
+        setSelectedShopState(null);
       }
       }
-    } else if (stores.length > 0 && !selectedShop) {
-      // Auto-select first store if none selected
-      setSelectedShopState(stores[0]);
+    } else if (activeStores.length > 0 && !selectedShop) {
+      // Auto-select first active store if none selected
+      setSelectedShopState(activeStores[0]);
     }
     }
     setIsLoading(false);
     setIsLoading(false);
-  }, [stores]);
+  }, [stores, storesLoaded]);
 
 
   const setSelectedShop = (shop: StoreData | null) => {
   const setSelectedShop = (shop: StoreData | null) => {
     setSelectedShopState(shop);
     setSelectedShopState(shop);
@@ -50,7 +124,7 @@ export const ShopProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
   };
   };
 
 
   return (
   return (
-    <ShopContext.Provider value={{ selectedShop, setSelectedShop, stores, setStores, isLoading }}>
+    <ShopContext.Provider value={{ selectedShop, setSelectedShop, stores, setStores, isLoading, storesLoaded, refreshStores: fetchStores }}>
       {children}
       {children}
     </ShopContext.Provider>
     </ShopContext.Provider>
   );
   );

+ 51 - 1
shopcall.ai-main/src/i18n/locales/de.json

@@ -195,7 +195,46 @@
     "aiMenu": "KI-Konfiguration",
     "aiMenu": "KI-Konfiguration",
     "knowledgeBase": "Wissensdatenbank",
     "knowledgeBase": "Wissensdatenbank",
     "products": "Produkte",
     "products": "Produkte",
-    "websiteContent": "Website-Inhalte"
+    "websiteContent": "Website-Inhalte",
+    "customContent": "Benutzerdefinierter Inhalt",
+    "inactive": "Inaktiv"
+  },
+  "settings": {
+    "title": "Kontoeinstellungen",
+    "subtitle": "Verwalten Sie Ihre Kontodaten und Einstellungen",
+    "profileInfo": "Profilinformationen",
+    "fullName": "Vollständiger Name",
+    "companyName": "Firmenname",
+    "updateProfile": "Profil aktualisieren",
+    "updating": "Wird aktualisiert...",
+    "profileUpdated": "Profil erfolgreich aktualisiert!",
+    "changeEmail": "E-Mail ändern",
+    "currentEmail": "Aktuelle E-Mail",
+    "newEmail": "Neue E-Mail",
+    "updateEmail": "E-Mail aktualisieren",
+    "emailUpdateSent": "Bestätigungs-E-Mail gesendet! Bitte überprüfen Sie sowohl Ihre alte als auch Ihre neue E-Mail-Adresse, um die Änderung zu bestätigen.",
+    "changePassword": "Passwort ändern",
+    "newPassword": "Neues Passwort",
+    "confirmPassword": "Passwort bestätigen",
+    "passwordRequirement": "Passwort muss mindestens 8 Zeichen lang sein",
+    "updatePassword": "Passwort aktualisieren",
+    "passwordUpdated": "Passwort erfolgreich aktualisiert!",
+    "placeholders": {
+      "fullName": "Max Mustermann",
+      "companyName": "Musterfirma GmbH",
+      "newEmail": "neueemail@beispiel.de",
+      "newPassword": "Neues Passwort eingeben",
+      "confirmPassword": "Neues Passwort bestätigen"
+    },
+    "errors": {
+      "notAuthenticated": "Nicht authentifiziert",
+      "updateProfileFailed": "Profil konnte nicht aktualisiert werden",
+      "invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
+      "updateEmailFailed": "E-Mail konnte nicht aktualisiert werden",
+      "passwordTooShort": "Passwort muss mindestens 8 Zeichen lang sein",
+      "passwordsDoNotMatch": "Passwörter stimmen nicht überein",
+      "updatePasswordFailed": "Passwort konnte nicht aktualisiert werden"
+    }
   },
   },
   "common": {
   "common": {
     "loading": "Wird geladen...",
     "loading": "Wird geladen...",
@@ -1410,6 +1449,17 @@
       }
       }
     }
     }
   },
   },
+  "storeProtectedRoute": {
+    "loadingStores": "Shops werden geladen...",
+    "noStoresTitle": "Keine Shops verbunden",
+    "noStoresDescription": "Sie müssen einen Shop verbinden, um diese Funktion zu nutzen.",
+    "connectStore": "Shop verbinden",
+    "noStoreSelectedTitle": "Kein Shop ausgewählt",
+    "noStoreSelectedDescription": "Bitte wählen Sie einen aktiven Shop aus der Seitenleiste aus, um auf diese Funktion zuzugreifen.",
+    "manageStores": "Shops verwalten",
+    "storeInactiveTitle": "Shop ist inaktiv",
+    "storeInactiveDescription": "Der ausgewählte Shop wurde getrennt oder deaktiviert. Bitte wählen Sie einen anderen Shop aus oder verbinden Sie diesen erneut."
+  },
   "privateRoute": {
   "privateRoute": {
     "aiPowered": "KI-gestützter Kundendienst",
     "aiPowered": "KI-gestützter Kundendienst",
     "securingSession": "Session wird gesichert",
     "securingSession": "Session wird gesichert",

+ 50 - 1
shopcall.ai-main/src/i18n/locales/en.json

@@ -206,7 +206,45 @@
     "knowledgeBase": "Knowledge Base",
     "knowledgeBase": "Knowledge Base",
     "products": "Products",
     "products": "Products",
     "websiteContent": "Website Content",
     "websiteContent": "Website Content",
-    "customContent": "Custom Content"
+    "customContent": "Custom Content",
+    "inactive": "Inactive"
+  },
+  "settings": {
+    "title": "Account Settings",
+    "subtitle": "Manage your account details and preferences",
+    "profileInfo": "Profile Information",
+    "fullName": "Full Name",
+    "companyName": "Company Name",
+    "updateProfile": "Update Profile",
+    "updating": "Updating...",
+    "profileUpdated": "Profile updated successfully!",
+    "changeEmail": "Change Email",
+    "currentEmail": "Current Email",
+    "newEmail": "New Email",
+    "updateEmail": "Update Email",
+    "emailUpdateSent": "Confirmation email sent! Please check both your old and new email addresses to confirm the change.",
+    "changePassword": "Change Password",
+    "newPassword": "New Password",
+    "confirmPassword": "Confirm Password",
+    "passwordRequirement": "Password must be at least 8 characters",
+    "updatePassword": "Update Password",
+    "passwordUpdated": "Password updated successfully!",
+    "placeholders": {
+      "fullName": "John Doe",
+      "companyName": "Acme Inc.",
+      "newEmail": "newemail@example.com",
+      "newPassword": "Enter new password",
+      "confirmPassword": "Confirm new password"
+    },
+    "errors": {
+      "notAuthenticated": "Not authenticated",
+      "updateProfileFailed": "Failed to update profile",
+      "invalidEmail": "Please enter a valid email address",
+      "updateEmailFailed": "Failed to update email",
+      "passwordTooShort": "Password must be at least 8 characters long",
+      "passwordsDoNotMatch": "Passwords do not match",
+      "updatePasswordFailed": "Failed to update password"
+    }
   },
   },
   "common": {
   "common": {
     "loading": "Loading...",
     "loading": "Loading...",
@@ -1567,6 +1605,17 @@
       }
       }
     }
     }
   },
   },
+  "storeProtectedRoute": {
+    "loadingStores": "Loading stores...",
+    "noStoresTitle": "No Stores Connected",
+    "noStoresDescription": "You need to connect a store to access this feature.",
+    "connectStore": "Connect a Store",
+    "noStoreSelectedTitle": "No Store Selected",
+    "noStoreSelectedDescription": "Please select an active store from the sidebar to access this feature.",
+    "manageStores": "Manage Stores",
+    "storeInactiveTitle": "Store is Inactive",
+    "storeInactiveDescription": "The selected store has been disconnected or disabled. Please select another store or reconnect this one."
+  },
   "privateRoute": {
   "privateRoute": {
     "aiPowered": "AI-Powered Customer Service",
     "aiPowered": "AI-Powered Customer Service",
     "securingSession": "Securing Your Session",
     "securingSession": "Securing Your Session",

+ 50 - 1
shopcall.ai-main/src/i18n/locales/hu.json

@@ -196,7 +196,45 @@
     "knowledgeBase": "Tudásbázis",
     "knowledgeBase": "Tudásbázis",
     "products": "Termékek",
     "products": "Termékek",
     "websiteContent": "Weboldal Tartalom",
     "websiteContent": "Weboldal Tartalom",
-    "customContent": "Egyedi Tartalom"
+    "customContent": "Egyedi Tartalom",
+    "inactive": "Inaktív"
+  },
+  "settings": {
+    "title": "Fiókbeállítások",
+    "subtitle": "Fiókadatok és beállítások kezelése",
+    "profileInfo": "Profilinformációk",
+    "fullName": "Teljes név",
+    "companyName": "Cégnév",
+    "updateProfile": "Profil frissítése",
+    "updating": "Frissítés...",
+    "profileUpdated": "Profil sikeresen frissítve!",
+    "changeEmail": "E-mail módosítása",
+    "currentEmail": "Jelenlegi e-mail",
+    "newEmail": "Új e-mail",
+    "updateEmail": "E-mail frissítése",
+    "emailUpdateSent": "Megerősítő e-mail elküldve! Kérjük, ellenőrizze a régi és az új e-mail címét a változtatás megerősítéséhez.",
+    "changePassword": "Jelszó módosítása",
+    "newPassword": "Új jelszó",
+    "confirmPassword": "Jelszó megerősítése",
+    "passwordRequirement": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
+    "updatePassword": "Jelszó frissítése",
+    "passwordUpdated": "Jelszó sikeresen frissítve!",
+    "placeholders": {
+      "fullName": "Kovács János",
+      "companyName": "Példa Kft.",
+      "newEmail": "ujemail@pelda.hu",
+      "newPassword": "Új jelszó megadása",
+      "confirmPassword": "Új jelszó megerősítése"
+    },
+    "errors": {
+      "notAuthenticated": "Nincs bejelentkezve",
+      "updateProfileFailed": "Nem sikerült frissíteni a profilt",
+      "invalidEmail": "Kérjük, adjon meg egy érvényes e-mail címet",
+      "updateEmailFailed": "Nem sikerült frissíteni az e-mail címet",
+      "passwordTooShort": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
+      "passwordsDoNotMatch": "A jelszavak nem egyeznek",
+      "updatePasswordFailed": "Nem sikerült frissíteni a jelszót"
+    }
   },
   },
   "common": {
   "common": {
     "loading": "Betöltés...",
     "loading": "Betöltés...",
@@ -1682,6 +1720,17 @@
     "playStereo": "Sztereó Felvétel Lejátszása",
     "playStereo": "Sztereó Felvétel Lejátszása",
     "noRecording": "Nincs elérhető felvétel"
     "noRecording": "Nincs elérhető felvétel"
   },
   },
+  "storeProtectedRoute": {
+    "loadingStores": "Boltok betöltése...",
+    "noStoresTitle": "Nincsenek csatlakoztatott boltok",
+    "noStoresDescription": "A funkció eléréséhez csatlakoztatnia kell egy boltot.",
+    "connectStore": "Bolt csatlakoztatása",
+    "noStoreSelectedTitle": "Nincs bolt kiválasztva",
+    "noStoreSelectedDescription": "Kérjük, válasszon ki egy aktív boltot az oldalsávból a funkció eléréséhez.",
+    "manageStores": "Boltok kezelése",
+    "storeInactiveTitle": "A bolt inaktív",
+    "storeInactiveDescription": "A kiválasztott bolt le lett kapcsolva vagy deaktiválva. Kérjük, válasszon másik boltot vagy csatlakoztassa újra."
+  },
   "privateRoute": {
   "privateRoute": {
     "aiPowered": "AI-alapú Ügyfélszolgálat",
     "aiPowered": "AI-alapú Ügyfélszolgálat",
     "securingSession": "Munkamenet Védelme",
     "securingSession": "Munkamenet Védelme",

+ 8 - 383
shopcall.ai-main/src/pages/Settings.tsx

@@ -1,391 +1,16 @@
-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 as UserIcon, Building } from "lucide-react";
-import { supabase } from "@/lib/supabase";
-import { useTranslation } from "react-i18next";
-interface User {
-  id: string;
-  email?: string;
-  user_metadata?: {
-    full_name?: string;
-    company_name?: string;
-    [key: string]: unknown;
-  };
-  [key: string]: unknown;
-}
 
 
-interface Profile {
-  id: string;
-  full_name: string | null;
-  username: string | null;
-  email: string | null;
-  company_name: string | null;
-  is_verified: boolean | null;
-  created_at: string;
-  updated_at: string;
-}
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { AppSidebar } from "@/components/AppSidebar";
+import { SettingsContent } from "@/components/SettingsContent";
 
 
 const Settings = () => {
 const Settings = () => {
-  const { t } = useTranslation();
-  const [isLoading, setIsLoading] = useState(false);
-  const [user, setUser] = useState<User | null>(null);
-  const [profile, setProfile] = useState<Profile | null>(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: unknown) {
-      console.error("Error updating profile:", error);
-      setErrorMessage((error as 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: unknown) {
-      console.error("Error updating email:", error);
-      setErrorMessage((error as 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: unknown) {
-      console.error("Error updating password:", error);
-      setErrorMessage((error as Error)?.message || "Failed to update password");
-    } finally {
-      setIsLoading(false);
-    }
-  };
-
   return (
   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">
-                  <UserIcon 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>
+    <SidebarProvider>
+      <div className="min-h-screen flex w-full">
+        <AppSidebar />
+        <SettingsContent />
       </div>
       </div>
-    </div>
+    </SidebarProvider>
   );
   );
 };
 };
 
 

+ 40 - 2
supabase/functions/_shared/scraper-client.ts

@@ -48,6 +48,39 @@ export interface ScraperContent {
   metadata?: Record<string, any>;
   metadata?: Record<string, any>;
 }
 }
 
 
+// Raw content item from scraper API /results endpoint
+export interface ScraperResultItem {
+  url: string;
+  content: string;
+  changed: boolean;
+  last_updated: string;
+  title?: string;
+}
+
+// Response structure from /api/shops/:id/results
+export interface ScraperResultsResponse {
+  shop_id: string;
+  filters: {
+    limit: number;
+    offset: number;
+    date_from: string | null;
+    date_to: string | null;
+    content_type: string | null;
+  };
+  pagination: {
+    total_returned: number;
+    limit: number;
+    offset: number;
+    has_more: boolean;
+  };
+  results: {
+    shipping_informations?: ScraperResultItem[];
+    contacts?: ScraperResultItem[];
+    terms_of_conditions?: ScraperResultItem[];
+    faq?: ScraperResultItem[];
+  };
+}
+
 export interface ScraperCustomUrl {
 export interface ScraperCustomUrl {
   id: string;
   id: string;
   url: string;
   url: string;
@@ -74,6 +107,7 @@ export interface ContentFilter {
   date_from?: string;
   date_from?: string;
   date_to?: string;
   date_to?: string;
   limit?: number;
   limit?: number;
+  offset?: number;
 }
 }
 
 
 /**
 /**
@@ -169,8 +203,9 @@ export class ScraperClient {
 
 
   /**
   /**
    * Get scraped content for a shop with optional filtering
    * Get scraped content for a shop with optional filtering
+   * Returns the new response format with shop_id, filters, pagination, and results
    */
    */
-  async getShopContent(shopId: string, filter?: ContentFilter): Promise<{ content: ScraperContent[] }> {
+  async getShopContent(shopId: string, filter?: ContentFilter): Promise<ScraperResultsResponse> {
     const params = new URLSearchParams();
     const params = new URLSearchParams();
 
 
     if (filter?.content_type) {
     if (filter?.content_type) {
@@ -185,9 +220,12 @@ export class ScraperClient {
     if (filter?.limit) {
     if (filter?.limit) {
       params.append('limit', filter.limit.toString());
       params.append('limit', filter.limit.toString());
     }
     }
+    if (filter?.offset) {
+      params.append('offset', filter.offset.toString());
+    }
 
 
     const endpoint = `/shops/${shopId}/results${params.toString() ? `?${params.toString()}` : ''}`;
     const endpoint = `/shops/${shopId}/results${params.toString() ? `?${params.toString()}` : ''}`;
-    return await this.makeRequest<{ content: ScraperContent[] }>(endpoint);
+    return await this.makeRequest<ScraperResultsResponse>(endpoint);
   }
   }
 
 
   /**
   /**

+ 11 - 5
supabase/functions/scraper-management/index.ts

@@ -10,7 +10,7 @@
  */
  */
 
 
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.3';
-import { createScraperClient, validateSameDomain } from '../_shared/scraper-client.ts';
+import { createScraperClient, validateSameDomain, ScraperResultItem } from '../_shared/scraper-client.ts';
 
 
 import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 import { getCorsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts';
 
 
@@ -314,11 +314,13 @@ Deno.serve(async (req) => {
         const dateFrom = url.searchParams.get('date_from');
         const dateFrom = url.searchParams.get('date_from');
         const dateTo = url.searchParams.get('date_to');
         const dateTo = url.searchParams.get('date_to');
         const limit = url.searchParams.get('limit');
         const limit = url.searchParams.get('limit');
+        const offset = url.searchParams.get('offset');
 
 
         if (contentType) filter.content_type = contentType;
         if (contentType) filter.content_type = contentType;
         if (dateFrom) filter.date_from = dateFrom;
         if (dateFrom) filter.date_from = dateFrom;
         if (dateTo) filter.date_to = dateTo;
         if (dateTo) filter.date_to = dateTo;
         if (limit) filter.limit = parseInt(limit, 10);
         if (limit) filter.limit = parseInt(limit, 10);
+        if (offset) filter.offset = parseInt(offset, 10);
 
 
         const rawContent = await scraperClient.getShopContent(store.id, filter);
         const rawContent = await scraperClient.getShopContent(store.id, filter);
 
 
@@ -327,12 +329,13 @@ Deno.serve(async (req) => {
         const results = rawContent.results || {};
         const results = rawContent.results || {};
 
 
         // Helper function to transform content items
         // Helper function to transform content items
-        const transformContentItems = (items: any[], type: string) => {
-          return (items || []).map((item: any, index: number) => ({
+        // Now uses the title from the API response if available
+        const transformContentItems = (items: ScraperResultItem[] | undefined, type: string) => {
+          return (items || []).map((item: ScraperResultItem, index: number) => ({
             id: `${type}-${index}-${item.url}`,
             id: `${type}-${index}-${item.url}`,
             url: item.url,
             url: item.url,
             content_type: type,
             content_type: type,
-            title: item.url.split('/').pop()?.replace(/-/g, ' ') || type,
+            title: item.title || item.url.split('/').pop()?.replace(/-/g, ' ') || type,
             content: item.content || '',
             content: item.content || '',
             scraped_at: item.last_updated || new Date().toISOString(),
             scraped_at: item.last_updated || new Date().toISOString(),
             metadata: {
             metadata: {
@@ -357,7 +360,10 @@ Deno.serve(async (req) => {
         }
         }
 
 
         return new Response(
         return new Response(
-          JSON.stringify({ content: contentArray }),
+          JSON.stringify({
+            content: contentArray,
+            pagination: rawContent.pagination,
+          }),
           { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
           { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
         );
         );
       }
       }

+ 177 - 68
supabase/functions/webhook-shoprenter-uninstall/index.ts

@@ -1,65 +1,109 @@
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
-import { wrapHandler, logError } from '../_shared/error-handler.ts'
-import { createHmac, timingSafeEqual } from 'https://deno.land/std@0.168.0/node/crypto.ts'
 
 
 const corsHeaders = {
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
   'Access-Control-Allow-Origin': '*',
   'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
   'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
 }
 }
 
 
+// Helper function to convert ArrayBuffer to hex string
+function bufferToHex(buffer: ArrayBuffer): string {
+  const byteArray = new Uint8Array(buffer);
+  return Array.from(byteArray)
+    .map((byte) => byte.toString(16).padStart(2, "0"))
+    .join("");
+}
+
+// Calculate HMAC-SHA256 using Deno's native crypto.subtle API
+async function calculateHmacSha256(secret: string, message: string): Promise<string> {
+  const encoder = new TextEncoder();
+  const keyData = encoder.encode(secret);
+  const key = await crypto.subtle.importKey(
+    "raw",
+    keyData,
+    { name: "HMAC", hash: { name: "SHA-256" } },
+    false,
+    ["sign"]
+  );
+  const messageData = encoder.encode(message);
+  const signature = await crypto.subtle.sign("HMAC", key, messageData);
+  return bufferToHex(signature);
+}
+
 // Validate HMAC signature from ShopRenter
 // Validate HMAC signature from ShopRenter
-function validateHMAC(query: Record<string, string>, clientSecret: string): boolean {
-  const { hmac, ...params } = query
+// IMPORTANT: Preserve original parameter order from URL (same as oauth-shoprenter-callback)
+async function validateHMAC(rawQueryString: string, clientSecret: string, clientId: string): Promise<boolean> {
+  if (!clientSecret) {
+    console.error('[ShopRenter] Client secret is empty or undefined')
+    return false
+  }
 
 
-  if (!hmac) {
+  // Parse the query string to get the hmac value
+  const params = new URLSearchParams(rawQueryString)
+  const hmacValue = params.get('hmac')
+  if (!hmacValue) {
     console.error('[ShopRenter] HMAC missing from request')
     console.error('[ShopRenter] HMAC missing from request')
     return false
     return false
   }
   }
 
 
-  // Build sorted query string without HMAC
-  const sortedParams = Object.keys(params)
-    .sort()
-    .map(key => `${key}=${params[key]}`)
+  // Build the params string by preserving the original order from the URL
+  // and excluding the hmac and app_url parameters
+  const paramsWithoutHmac = rawQueryString
+    .split('&')
+    .filter(param => {
+      const key = param.split('=')[0]
+      return key !== 'hmac' && key !== 'app_url'
+    })
     .join('&')
     .join('&')
 
 
-  // Calculate HMAC using sha256
-  const calculatedHmac = createHmac('sha256', clientSecret)
-    .update(sortedParams)
-    .digest('hex')
+  console.log(`[ShopRenter] HMAC validation - params (original order): ${paramsWithoutHmac}`)
+  console.log(`[ShopRenter] HMAC validation - client secret length: ${clientSecret.length}`)
 
 
-  // Timing-safe comparison
-  try {
-    return timingSafeEqual(
-      new TextEncoder().encode(calculatedHmac),
-      new TextEncoder().encode(hmac)
-    )
-  } catch (error) {
-    console.error('[ShopRenter] HMAC comparison error:', error)
-    return false
+  // Calculate HMAC using Deno's native crypto.subtle API (SHA-256)
+  const calculatedHmacWithSecret = await calculateHmacSha256(clientSecret, paramsWithoutHmac)
+
+  console.log(`[ShopRenter] HMAC validation - received hmac: ${hmacValue}`)
+  console.log(`[ShopRenter] HMAC validation - calculated hmac (with secret): ${calculatedHmacWithSecret}`)
+
+  // Compare HMACs
+  const resultWithSecret = calculatedHmacWithSecret === hmacValue
+  console.log(`[ShopRenter] HMAC validation result (with secret): ${resultWithSecret}`)
+
+  if (resultWithSecret) {
+    return true
   }
   }
+
+  // Fallback: try with Client ID (in case of documentation confusion)
+  const calculatedHmacWithId = await calculateHmacSha256(clientId, paramsWithoutHmac)
+  console.log(`[ShopRenter] HMAC validation - calculated hmac (with id): ${calculatedHmacWithId}`)
+
+  const resultWithId = calculatedHmacWithId === hmacValue
+  console.log(`[ShopRenter] HMAC validation result (with id): ${resultWithId}`)
+
+  return resultWithId
 }
 }
 
 
-// Validate timestamp to prevent replay attacks
+// Validate timestamp - lenient validation (logs warnings but doesn't reject)
+// HMAC validation is the primary security check
 function validateTimestamp(timestamp: string, maxAgeSeconds = 300): boolean {
 function validateTimestamp(timestamp: string, maxAgeSeconds = 300): boolean {
   const requestTime = parseInt(timestamp, 10)
   const requestTime = parseInt(timestamp, 10)
   const currentTime = Math.floor(Date.now() / 1000)
   const currentTime = Math.floor(Date.now() / 1000)
   const age = currentTime - requestTime
   const age = currentTime - requestTime
 
 
-  if (age < 0) {
-    console.error('[ShopRenter] Request timestamp is in the future')
-    return false
+  if (age < -60) {
+    // Allow up to 60 seconds of clock skew for future timestamps
+    console.warn(`[ShopRenter] Request timestamp is in the future by ${-age}s - allowing due to potential clock skew`)
   }
   }
 
 
   if (age > maxAgeSeconds) {
   if (age > maxAgeSeconds) {
-    console.error(`[ShopRenter] Request timestamp too old: ${age}s > ${maxAgeSeconds}s`)
-    return false
+    console.warn(`[ShopRenter] Request timestamp is old: ${age}s > ${maxAgeSeconds}s - allowing due to potential ShopRenter timestamp issues`)
   }
   }
 
 
+  // Always return true - we rely on HMAC validation for security
   return true
   return true
 }
 }
 
 
-serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
+serve(async (req) => {
   if (req.method === 'OPTIONS') {
   if (req.method === 'OPTIONS') {
     return new Response('ok', { headers: corsHeaders })
     return new Response('ok', { headers: corsHeaders })
   }
   }
@@ -67,7 +111,6 @@ serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
   try {
   try {
     const url = new URL(req.url)
     const url = new URL(req.url)
     const shopname = url.searchParams.get('shopname')
     const shopname = url.searchParams.get('shopname')
-    const code = url.searchParams.get('code')
     const timestamp = url.searchParams.get('timestamp')
     const timestamp = url.searchParams.get('timestamp')
     const hmac = url.searchParams.get('hmac')
     const hmac = url.searchParams.get('hmac')
 
 
@@ -81,41 +124,31 @@ serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
       )
       )
     }
     }
 
 
-    // Get environment variables
-    const shoprenterClientSecret = Deno.env.get('SHOPRENTER_APP_CLIENT_SECRET')
+    // Get environment variables (support both naming conventions with fallback)
+    const shoprenterClientId = Deno.env.get('SHOPRENTER_APP_CLIENT_ID') || Deno.env.get('SHOPRENTER_CLIENT_ID')
+    const shoprenterClientSecret = Deno.env.get('SHOPRENTER_APP_CLIENT_SECRET') || Deno.env.get('SHOPRENTER_CLIENT_SECRET')
     const supabaseUrl = Deno.env.get('SUPABASE_URL')!
     const supabaseUrl = Deno.env.get('SUPABASE_URL')!
     const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
     const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
 
 
-    if (!shoprenterClientSecret) {
-      console.error('SHOPRENTER_APP_CLIENT_SECRET not configured')
-      // Still return 200 to prevent retries
-      return new Response(
-        JSON.stringify({ message: 'Configuration error' }),
-        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
-      )
-    }
+    // Log which environment variables are being used
+    console.log(`[ShopRenter] Using client ID from: ${Deno.env.get('SHOPRENTER_APP_CLIENT_ID') ? 'SHOPRENTER_APP_CLIENT_ID' : 'SHOPRENTER_CLIENT_ID'}`)
+    console.log(`[ShopRenter] Using client secret from: ${Deno.env.get('SHOPRENTER_APP_CLIENT_SECRET') ? 'SHOPRENTER_APP_CLIENT_SECRET' : 'SHOPRENTER_CLIENT_SECRET'}`)
 
 
-    // Validate timestamp
-    if (!validateTimestamp(timestamp)) {
-      console.error('[ShopRenter] Timestamp validation failed')
+    if (!shoprenterClientId || !shoprenterClientSecret) {
+      console.error('ShopRenter client credentials not configured. Set either SHOPRENTER_APP_CLIENT_ID/SECRET or SHOPRENTER_CLIENT_ID/SECRET')
       // Still return 200 to prevent retries
       // Still return 200 to prevent retries
       return new Response(
       return new Response(
-        JSON.stringify({ message: 'Timestamp invalid' }),
+        JSON.stringify({ message: 'Configuration error' }),
         { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
         { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       )
       )
     }
     }
 
 
-    // Validate HMAC
-    const queryParams: Record<string, string> = {
-      shopname,
-      timestamp,
-      hmac
-    }
-    if (code) {
-      queryParams.code = code
-    }
+    // Validate timestamp (lenient - logs only)
+    validateTimestamp(timestamp)
 
 
-    if (!validateHMAC(queryParams, shoprenterClientSecret)) {
+    // Validate HMAC using the raw query string to preserve parameter order
+    const rawQueryString = url.search.substring(1) // Remove leading '?'
+    if (!(await validateHMAC(rawQueryString, shoprenterClientSecret, shoprenterClientId))) {
       console.error('[ShopRenter] HMAC validation failed')
       console.error('[ShopRenter] HMAC validation failed')
       // Still return 200 to prevent retries
       // Still return 200 to prevent retries
       return new Response(
       return new Response(
@@ -124,13 +157,15 @@ serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
       )
       )
     }
     }
 
 
+    console.log('[ShopRenter] HMAC validation passed')
+
     // Create Supabase client with service role key
     // Create Supabase client with service role key
     const supabase = createClient(supabaseUrl, supabaseServiceKey)
     const supabase = createClient(supabaseUrl, supabaseServiceKey)
 
 
-    // Find store by shopname
+    // Find store by shopname (include scraper config for disabling)
     const { data: store, error: storeError } = await supabase
     const { data: store, error: storeError } = await supabase
       .from('stores')
       .from('stores')
-      .select('id')
+      .select('id, alt_data, scraper_api_url, scraper_api_secret, scraper_registered')
       .eq('platform_name', 'shoprenter')
       .eq('platform_name', 'shoprenter')
       .eq('store_name', shopname)
       .eq('store_name', shopname)
       .maybeSingle()
       .maybeSingle()
@@ -145,32 +180,106 @@ serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
     }
     }
 
 
     if (store) {
     if (store) {
-      // Deactivate store
+      const uninstallTimestamp = new Date().toISOString()
+
+      // Merge existing alt_data with uninstall info
+      const updatedAltData = {
+        ...(store.alt_data || {}),
+        uninstalled: true,
+        uninstalled_at: uninstallTimestamp
+      }
+
+      // Disable store - DO NOT DELETE anything
       const { error: updateError } = await supabase
       const { error: updateError } = await supabase
         .from('stores')
         .from('stores')
         .update({
         .update({
-          alt_data: { uninstalled: true, uninstalled_at: new Date().toISOString() }
+          is_active: false,
+          qdrant_sync_enabled: false,
+          scraper_enabled: false,
+          sync_status: 'idle',
+          alt_data: updatedAltData
         })
         })
         .eq('id', store.id)
         .eq('id', store.id)
 
 
       if (updateError) {
       if (updateError) {
         console.error('[ShopRenter] Error updating store:', updateError)
         console.error('[ShopRenter] Error updating store:', updateError)
+      } else {
+        console.log(`[ShopRenter] Store ${shopname} disabled (is_active=false, qdrant_sync_enabled=false, scraper_enabled=false)`)
+      }
+
+      // Disable sync config
+      const { error: syncConfigError } = await supabase
+        .from('store_sync_config')
+        .update({ enabled: false })
+        .eq('store_id', store.id)
+
+      if (syncConfigError) {
+        console.error('[ShopRenter] Error disabling sync config:', syncConfigError)
+      } else {
+        console.log(`[ShopRenter] Sync config disabled for store ${shopname}`)
       }
       }
 
 
-      // Delete associated data
-      // Delete cached products
-      await supabase
-        .from('shoprenter_products_cache')
-        .delete()
+      // Disable tokens (not delete)
+      const { error: tokensError } = await supabase
+        .from('shoprenter_tokens')
+        .update({ is_active: false })
         .eq('store_id', store.id)
         .eq('store_id', store.id)
 
 
-      // Delete webhooks
-      await supabase
+      if (tokensError) {
+        console.error('[ShopRenter] Error disabling tokens:', tokensError)
+      } else {
+        console.log(`[ShopRenter] Tokens disabled for store ${shopname}`)
+      }
+
+      // Disable webhooks (not delete)
+      const { error: webhooksError } = await supabase
         .from('shoprenter_webhooks')
         .from('shoprenter_webhooks')
-        .delete()
+        .update({ is_active: false })
         .eq('store_id', store.id)
         .eq('store_id', store.id)
 
 
-      console.log(`[ShopRenter] Store ${shopname} uninstalled successfully`)
+      if (webhooksError) {
+        console.error('[ShopRenter] Error disabling webhooks:', webhooksError)
+      } else {
+        console.log(`[ShopRenter] Webhooks disabled for store ${shopname}`)
+      }
+
+      // Disable scheduled scraping at the scraper microservice
+      if (store.scraper_registered) {
+        try {
+          // Get scraper API config (per-store or global defaults)
+          const scraperApiUrl = store.scraper_api_url || Deno.env.get('DEFAULT_SCRAPER_API_URL')
+          const scraperApiSecret = store.scraper_api_secret || Deno.env.get('DEFAULT_SCRAPER_API_SECRET')
+
+          if (scraperApiUrl && scraperApiSecret) {
+            console.log(`[ShopRenter] Disabling scheduled scraping for store ${store.id}`)
+
+            const scraperResponse = await fetch(`${scraperApiUrl}/api/shops/${store.id}/schedule`, {
+              method: 'PATCH',
+              headers: {
+                'Authorization': `Bearer ${scraperApiSecret}`,
+                'Content-Type': 'application/json',
+              },
+              body: JSON.stringify({ enabled: false }),
+            })
+
+            if (scraperResponse.ok) {
+              console.log(`[ShopRenter] Scheduled scraping disabled for store ${shopname}`)
+            } else {
+              const errorText = await scraperResponse.text()
+              console.error(`[ShopRenter] Error disabling scraper scheduling (${scraperResponse.status}): ${errorText}`)
+            }
+          } else {
+            console.warn(`[ShopRenter] Scraper API not configured, skipping scraper disable for store ${shopname}`)
+          }
+        } catch (scraperError) {
+          // Don't fail the webhook if scraper call fails
+          console.error(`[ShopRenter] Error calling scraper API:`, scraperError)
+        }
+      } else {
+        console.log(`[ShopRenter] Store ${shopname} not registered with scraper, skipping scraper disable`)
+      }
+
+      console.log(`[ShopRenter] Store ${shopname} uninstalled successfully (all features disabled, data preserved)`)
     } else {
     } else {
       console.log(`[ShopRenter] Store ${shopname} not found in database`)
       console.log(`[ShopRenter] Store ${shopname} not found in database`)
     }
     }
@@ -189,4 +298,4 @@ serve(wrapHandler('webhook-shoprenter-uninstall', async (req) => {
       { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
     )
     )
   }
   }
-}))
+})