Explorar el Código

feat: add WooCommerce integration UI with OAuth callback handling and store management #12

- Add OAuth callback handling for WooCommerce connection flow
- Display WooCommerce and WordPress version information in store list
- Implement disconnect functionality with confirmation dialog
- Create API Edge Function for store listing and deletion
- Add toast notifications for success/error feedback
- Enhance store table with platform-specific badges and colors
- Add Trash2 icon for disconnect action
Claude hace 5 meses
padre
commit
643266288d

+ 144 - 31
shopcall.ai-main/src/components/IntegrationsContent.tsx

@@ -6,9 +6,11 @@ import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Switch } from "@/components/ui/switch";
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
-import { Plus, Settings, Store, Bot, PhoneCall, Globe, Zap, ShoppingBag, Loader2 } from "lucide-react";
+import { Plus, Settings, Store, Bot, PhoneCall, Globe, Zap, ShoppingBag, Loader2, Trash2, Info } from "lucide-react";
 import { ShopRenterConnect } from "./ShopRenterConnect";
+import { WooCommerceConnect } from "./WooCommerceConnect";
 import { API_URL } from "@/lib/config";
+import { useToast } from "@/hooks/use-toast";
 
 interface ConnectedStore {
   id: string;
@@ -18,6 +20,13 @@ interface ConnectedStore {
   is_active: boolean | null;
   phone_number: string | null;
   created_at: string | null;
+  alt_data?: {
+    wcVersion?: string;
+    wpVersion?: string;
+    apiVersion?: string;
+    connectedAt?: string;
+    [key: string]: any;
+  };
 }
 
 export function IntegrationsContent() {
@@ -25,38 +34,80 @@ export function IntegrationsContent() {
   const [loading, setLoading] = useState(true);
   const [showConnectDialog, setShowConnectDialog] = useState(false);
   const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
+  const { toast } = useToast();
 
-  useEffect(() => {
-    const fetchStores = async () => {
-      try {
-        const sessionData = localStorage.getItem('session_data');
-        if (!sessionData) {
-          throw new Error('No session data found');
-        }
-
-        const session = JSON.parse(sessionData);
-        const response = await fetch(`${API_URL}/api/stores`, {
-          headers: {
-            'Authorization': `Bearer ${session.access_token}`,
-            'Content-Type': 'application/json'
-          }
-        });
+  const fetchStores = async () => {
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        throw new Error('No session data found');
+      }
 
-        if (!response.ok) {
-          throw new Error('Failed to fetch stores');
+      const session = JSON.parse(sessionData);
+      const response = await fetch(`${API_URL}/api/stores`, {
+        headers: {
+          'Authorization': `Bearer ${session.access_token}`,
+          'Content-Type': 'application/json'
         }
+      });
 
-        const data = await response.json();
-        if (data.success) {
-          setConnectedShops(data.stores || []);
-        }
-      } catch (error) {
-        console.error('Error fetching stores:', error);
-      } finally {
-        setLoading(false);
+      if (!response.ok) {
+        throw new Error('Failed to fetch stores');
+      }
+
+      const data = await response.json();
+      if (data.success) {
+        setConnectedShops(data.stores || []);
       }
-    };
+    } catch (error) {
+      console.error('Error fetching stores:', error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // Handle OAuth callbacks
+  useEffect(() => {
+    const params = new URLSearchParams(window.location.search);
+
+    // Handle WooCommerce OAuth callback
+    if (params.get('wc_connected') === 'true') {
+      const storeName = params.get('store');
+      toast({
+        title: "WooCommerce Connected!",
+        description: storeName
+          ? `Successfully connected ${decodeURIComponent(storeName)}`
+          : "Your WooCommerce store has been connected successfully.",
+      });
+      // Clean up URL
+      window.history.replaceState({}, '', '/webshops');
+      // Refresh stores list
+      fetchStores();
+    }
+
+    // Handle errors
+    const error = params.get('error');
+    if (error) {
+      const errorMessages: Record<string, string> = {
+        'woocommerce_oauth_rejected': 'WooCommerce connection was cancelled. Please try again.',
+        'woocommerce_oauth_failed': 'Failed to connect to WooCommerce. Please try again.',
+        'woocommerce_connection_failed': 'Could not connect to your WooCommerce store. Please check your store URL and try again.',
+        'invalid_store_url': 'Invalid store URL. Please check the URL and try again.',
+        'failed_to_save': 'Failed to save store connection. Please try again.',
+        'internal_error': 'An internal error occurred. Please try again later.',
+      };
+
+      toast({
+        title: "Connection Failed",
+        description: errorMessages[error] || "An unexpected error occurred. Please try again.",
+        variant: "destructive",
+      });
+      // Clean up URL
+      window.history.replaceState({}, '', '/webshops');
+    }
+  }, [toast]);
 
+  useEffect(() => {
     fetchStores();
   }, []);
 
@@ -93,6 +144,47 @@ export function IntegrationsContent() {
     setSelectedPlatform(null);
   };
 
+  const handleDisconnectStore = async (storeId: string, storeName: string) => {
+    if (!confirm(`Are you sure you want to disconnect ${storeName}? This action cannot be undone.`)) {
+      return;
+    }
+
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        throw new Error('No session data found');
+      }
+
+      const session = JSON.parse(sessionData);
+      const response = await fetch(`${API_URL}/api/stores/${storeId}`, {
+        method: 'DELETE',
+        headers: {
+          'Authorization': `Bearer ${session.access_token}`,
+          'Content-Type': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to disconnect store');
+      }
+
+      toast({
+        title: "Store Disconnected",
+        description: `${storeName} has been disconnected successfully.`,
+      });
+
+      // Refresh stores list
+      fetchStores();
+    } catch (error) {
+      console.error('Error disconnecting store:', error);
+      toast({
+        title: "Disconnection Failed",
+        description: "Failed to disconnect the store. Please try again.",
+        variant: "destructive",
+      });
+    }
+  };
+
   return (
     <div className="flex-1 space-y-6 p-8 bg-slate-900">
       <div className="flex items-center justify-between">
@@ -146,6 +238,8 @@ export function IntegrationsContent() {
             </>
           ) : selectedPlatform === "shoprenter" ? (
             <ShopRenterConnect onClose={handleCloseDialog} />
+          ) : selectedPlatform === "woocommerce" ? (
+            <WooCommerceConnect onClose={handleCloseDialog} />
           ) : (
             <div className="text-center py-8">
               <p className="text-white">
@@ -270,8 +364,18 @@ export function IntegrationsContent() {
                       <tr key={shop.id} className="border-b border-slate-700/50 hover:bg-slate-700/30">
                         <td className="p-4">
                           <div>
-                            <div className="text-white font-medium">{shop.store_name || 'Unnamed Store'}</div>
+                            <div className="flex items-center gap-2">
+                              <div className="text-white font-medium">{shop.store_name || 'Unnamed Store'}</div>
+                              {shop.platform_name === 'woocommerce' && shop.alt_data?.wcVersion && (
+                                <Badge variant="outline" className="text-xs border-purple-500 text-purple-400">
+                                  WC {shop.alt_data.wcVersion}
+                                </Badge>
+                              )}
+                            </div>
                             <div className="text-slate-500 text-xs">{shop.store_url || '-'}</div>
+                            {shop.platform_name === 'woocommerce' && shop.alt_data?.wpVersion && (
+                              <div className="text-slate-600 text-xs mt-1">WordPress {shop.alt_data.wpVersion}</div>
+                            )}
                           </div>
                         </td>
                         <td className="p-4">
@@ -292,7 +396,11 @@ export function IntegrationsContent() {
                           </div>
                         </td>
                         <td className="p-4">
-                          <Badge className="bg-blue-500 text-white capitalize">
+                          <Badge className={`capitalize ${
+                            shop.platform_name === 'woocommerce' ? 'bg-purple-500' :
+                            shop.platform_name === 'shoprenter' ? 'bg-blue-500' :
+                            'bg-green-500'
+                          } text-white`}>
                             {shop.platform_name}
                           </Badge>
                         </td>
@@ -325,8 +433,13 @@ export function IntegrationsContent() {
                               <PhoneCall className="w-4 h-4 mr-1" />
                               Phone
                             </Button>
-                            <Button variant="ghost" size="sm" className="text-slate-400 hover:text-white">
-                              <Settings className="w-4 h-4" />
+                            <Button
+                              size="sm"
+                              variant="ghost"
+                              className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
+                              onClick={() => handleDisconnectStore(shop.id, shop.store_name || 'this store')}
+                            >
+                              <Trash2 className="w-4 h-4" />
                             </Button>
                           </div>
                         </td>

+ 120 - 0
supabase/functions/api/index.ts

@@ -0,0 +1,120 @@
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+}
+
+serve(async (req) => {
+  if (req.method === 'OPTIONS') {
+    return new Response('ok', { headers: corsHeaders })
+  }
+
+  try {
+    const url = new URL(req.url)
+    const path = url.pathname.replace('/api/', '')
+
+    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
+    const supabaseKey = Deno.env.get('SUPABASE_ANON_KEY')!
+    const supabase = createClient(supabaseUrl, supabaseKey)
+
+    // Get user from authorization header
+    const authHeader = req.headers.get('authorization')
+    if (!authHeader) {
+      return new Response(
+        JSON.stringify({ error: 'No authorization header' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    const token = authHeader.replace('Bearer ', '')
+    const { data: { user }, error: userError } = await supabase.auth.getUser(token)
+
+    if (userError || !user) {
+      return new Response(
+        JSON.stringify({ error: 'Invalid token' }),
+        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // GET /api/stores - List all stores for the user
+    if (path === 'stores' && req.method === 'GET') {
+      const { data: stores, error } = await supabase
+        .from('stores')
+        .select('*')
+        .eq('user_id', user.id)
+        .order('created_at', { ascending: false })
+
+      if (error) {
+        console.error('Error fetching stores:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch stores' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          stores: stores || []
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    // DELETE /api/stores/{id} - Delete a store
+    if (path.startsWith('stores/') && req.method === 'DELETE') {
+      const storeId = path.replace('stores/', '')
+
+      // Verify the store belongs to the user
+      const { data: store, error: fetchError } = await supabase
+        .from('stores')
+        .select('id')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (fetchError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Delete the store
+      const { error: deleteError } = await supabase
+        .from('stores')
+        .delete()
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+
+      if (deleteError) {
+        console.error('Error deleting store:', deleteError)
+        return new Response(
+          JSON.stringify({ error: 'Failed to delete store' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      return new Response(
+        JSON.stringify({
+          success: true,
+          message: 'Store deleted successfully'
+        }),
+        { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+      )
+    }
+
+    return new Response(
+      JSON.stringify({ error: 'Not found' }),
+      { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  } catch (error) {
+    console.error('Error:', error)
+    return new Response(
+      JSON.stringify({ error: 'Internal server error' }),
+      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+    )
+  }
+})