فهرست منبع

fix: resolve auth dialog stuck issue and add phone number selection #97

- Fixed auth loading state issue in IntegrationsRedirect component
- Added proper authentication check on component mount
- Added phone number selection dropdown for authenticated users
- Updated complete-shoprenter-install to accept phone_number_id from request
- Added loading states for auth check and phone number fetching

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 ماه پیش
والد
کامیت
d70c77ec02
2فایلهای تغییر یافته به همراه151 افزوده شده و 10 حذف شده
  1. 144 6
      shopcall.ai-main/src/pages/IntegrationsRedirect.tsx
  2. 7 4
      supabase/functions/complete-shoprenter-install/index.ts

+ 144 - 6
shopcall.ai-main/src/pages/IntegrationsRedirect.tsx

@@ -3,11 +3,13 @@ import { useNavigate, useSearchParams } from "react-router-dom";
 import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
-import { Store, LogIn, UserPlus, Loader2, CheckCircle, AlertCircle, ArrowRight } from "lucide-react";
+import { Store, LogIn, UserPlus, Loader2, CheckCircle, AlertCircle, ArrowRight, Phone } from "lucide-react";
 import { useAuth } from "@/components/context/AuthContext";
 import { useAuth } from "@/components/context/AuthContext";
 import { API_URL } from "@/lib/config";
 import { API_URL } from "@/lib/config";
 import { useToast } from "@/hooks/use-toast";
 import { useToast } from "@/hooks/use-toast";
 import { useTranslation } from "react-i18next";
 import { useTranslation } from "react-i18next";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Label } from "@/components/ui/label";
 
 
 interface PendingInstallation {
 interface PendingInstallation {
   installation_id: string;
   installation_id: string;
@@ -15,11 +17,19 @@ interface PendingInstallation {
   platform: 'shoprenter' | 'shopify' | 'woocommerce';
   platform: 'shoprenter' | 'shopify' | 'woocommerce';
 }
 }
 
 
+interface PhoneNumber {
+  id: string;
+  phone_number: string;
+  country_code: string;
+  monthly_price: number;
+  currency: string;
+  is_available: boolean;
+}
+
 export default function IntegrationsRedirect() {
 export default function IntegrationsRedirect() {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const navigate = useNavigate();
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
   const [searchParams] = useSearchParams();
-  const { isAuthenticated, loading: authLoading } = useAuth();
   const { toast } = useToast();
   const { toast } = useToast();
 
 
   const [pendingInstall, setPendingInstall] = useState<PendingInstallation | null>(null);
   const [pendingInstall, setPendingInstall] = useState<PendingInstallation | null>(null);
@@ -28,6 +38,55 @@ export default function IntegrationsRedirect() {
   const [completing, setCompleting] = useState(false);
   const [completing, setCompleting] = useState(false);
   const [validating, setValidating] = useState(false);
   const [validating, setValidating] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [error, setError] = useState<string | null>(null);
+  const [checkingAuth, setCheckingAuth] = useState(true);
+  const [isAuthenticated, setIsAuthenticated] = useState(false);
+
+  // Phone number selection state
+  const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
+  const [selectedPhoneNumber, setSelectedPhoneNumber] = useState<string>('');
+  const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
+
+  // Check authentication status on mount
+  useEffect(() => {
+    const checkAuthStatus = async () => {
+      setCheckingAuth(true);
+      const sessionData = localStorage.getItem('session_data');
+
+      if (!sessionData) {
+        setIsAuthenticated(false);
+        setCheckingAuth(false);
+        return;
+      }
+
+      try {
+        const session = JSON.parse(sessionData);
+        if (!session.success || !session.session?.access_token) {
+          setIsAuthenticated(false);
+          setCheckingAuth(false);
+          return;
+        }
+
+        // Validate token with backend
+        const response = await fetch(`${API_URL}/auth/check`, {
+          method: 'GET',
+          headers: {
+            'Content-Type': 'application/json',
+            'Authorization': `Bearer ${session.session.access_token}`,
+          }
+        });
+
+        const data = await response.json();
+        setIsAuthenticated(data.success === true);
+      } catch (err) {
+        console.error('Auth check failed:', err);
+        setIsAuthenticated(false);
+      } finally {
+        setCheckingAuth(false);
+      }
+    };
+
+    checkAuthStatus();
+  }, []);
 
 
   // Detect platform from URL parameters and validate HMAC
   // Detect platform from URL parameters and validate HMAC
   useEffect(() => {
   useEffect(() => {
@@ -110,9 +169,42 @@ export default function IntegrationsRedirect() {
     validateAndSetup();
     validateAndSetup();
   }, [searchParams, t]);
   }, [searchParams, t]);
 
 
+  // Fetch available phone numbers when user is authenticated
+  useEffect(() => {
+    const fetchPhoneNumbers = async () => {
+      if (!isAuthenticated || !pendingInstall) return;
+
+      setLoadingPhoneNumbers(true);
+      try {
+        const sessionData = localStorage.getItem('session_data');
+        if (!sessionData) return;
+
+        const session = JSON.parse(sessionData);
+        const response = await fetch(`${API_URL}/api/phone-numbers?available=true`, {
+          method: 'GET',
+          headers: {
+            'Authorization': `Bearer ${session.session.access_token}`,
+            'Content-Type': 'application/json'
+          }
+        });
+
+        if (response.ok) {
+          const data = await response.json();
+          setPhoneNumbers(data.phone_numbers || []);
+        }
+      } catch (err) {
+        console.error('Error fetching phone numbers:', err);
+      } finally {
+        setLoadingPhoneNumbers(false);
+      }
+    };
+
+    fetchPhoneNumbers();
+  }, [isAuthenticated, pendingInstall]);
+
   // Handle auth state changes
   // Handle auth state changes
   useEffect(() => {
   useEffect(() => {
-    if (authLoading) return;
+    if (checkingAuth) return;
 
 
     if (pendingInstall) {
     if (pendingInstall) {
       if (isAuthenticated) {
       if (isAuthenticated) {
@@ -123,7 +215,7 @@ export default function IntegrationsRedirect() {
         setShowAuthDialog(true);
         setShowAuthDialog(true);
       }
       }
     }
     }
-  }, [isAuthenticated, authLoading, pendingInstall]);
+  }, [isAuthenticated, checkingAuth, pendingInstall]);
 
 
   // Complete the installation
   // Complete the installation
   const completeInstallation = async () => {
   const completeInstallation = async () => {
@@ -144,7 +236,8 @@ export default function IntegrationsRedirect() {
           'Content-Type': 'application/json'
           'Content-Type': 'application/json'
         },
         },
         body: JSON.stringify({
         body: JSON.stringify({
-          installation_id: pendingInstall.installation_id
+          installation_id: pendingInstall.installation_id,
+          phone_number_id: selectedPhoneNumber || null
         })
         })
       });
       });
 
 
@@ -233,7 +326,7 @@ export default function IntegrationsRedirect() {
   }
   }
 
 
   // Show loading state
   // Show loading state
-  if (authLoading || validating || (!pendingInstall && !error)) {
+  if (checkingAuth || validating || (!pendingInstall && !error)) {
     return (
     return (
       <div className="min-h-screen flex items-center justify-center bg-slate-900">
       <div className="min-h-screen flex items-center justify-center bg-slate-900">
         <div className="text-center">
         <div className="text-center">
@@ -241,6 +334,8 @@ export default function IntegrationsRedirect() {
           <p className="text-slate-400">
           <p className="text-slate-400">
             {validating
             {validating
               ? t('integrations.oauth.validating', 'Validating security parameters...')
               ? t('integrations.oauth.validating', 'Validating security parameters...')
+              : checkingAuth
+              ? t('integrations.oauth.checkingAuth', 'Checking authentication...')
               : t('integrations.oauth.processing', 'Processing integration...')}
               : t('integrations.oauth.processing', 'Processing integration...')}
           </p>
           </p>
         </div>
         </div>
@@ -335,6 +430,49 @@ export default function IntegrationsRedirect() {
             </DialogDescription>
             </DialogDescription>
           </DialogHeader>
           </DialogHeader>
 
 
+          {/* Phone Number Selection */}
+          <div className="mt-4 space-y-3">
+            <div className="space-y-2">
+              <Label className="text-slate-300 flex items-center gap-2">
+                <Phone className="w-4 h-4" />
+                {t('integrations.oauth.selectPhoneNumber', 'Select Phone Number')}
+              </Label>
+              {loadingPhoneNumbers ? (
+                <div className="flex items-center justify-center py-3">
+                  <Loader2 className="w-5 h-5 text-cyan-500 animate-spin" />
+                  <span className="ml-2 text-sm text-slate-400">
+                    {t('integrations.oauth.loadingPhoneNumbers', 'Loading available phone numbers...')}
+                  </span>
+                </div>
+              ) : phoneNumbers.length > 0 ? (
+                <Select value={selectedPhoneNumber} onValueChange={setSelectedPhoneNumber}>
+                  <SelectTrigger className="bg-slate-700 border-slate-600 text-white">
+                    <SelectValue placeholder={t('integrations.oauth.selectPhonePlaceholder', 'Select a phone number')} />
+                  </SelectTrigger>
+                  <SelectContent className="bg-slate-700 border-slate-600">
+                    {phoneNumbers.map((phone) => (
+                      <SelectItem key={phone.id} value={phone.id} className="text-white hover:bg-slate-600">
+                        <div className="flex items-center justify-between w-full">
+                          <span>{phone.phone_number}</span>
+                          <span className="ml-2 text-sm text-slate-400">
+                            {phone.monthly_price > 0 ? `${phone.monthly_price} ${phone.currency}/mo` : t('common.free', 'Free')}
+                          </span>
+                        </div>
+                      </SelectItem>
+                    ))}
+                  </SelectContent>
+                </Select>
+              ) : (
+                <p className="text-sm text-slate-400 py-2">
+                  {t('integrations.oauth.noPhoneNumbersAvailable', 'No phone numbers available. You can add one later.')}
+                </p>
+              )}
+              <p className="text-xs text-slate-500">
+                {t('integrations.oauth.phoneNumberOptional', 'Optional - You can also assign a phone number later in settings.')}
+              </p>
+            </div>
+          </div>
+
           <div className="space-y-3 mt-4">
           <div className="space-y-3 mt-4">
             <Button
             <Button
               className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
               className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"

+ 7 - 4
supabase/functions/complete-shoprenter-install/index.ts

@@ -39,7 +39,7 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
 
 
     // Parse request body
     // Parse request body
     const body = await req.json()
     const body = await req.json()
-    const { installation_id } = body
+    const { installation_id, phone_number_id } = body
 
 
     if (!installation_id) {
     if (!installation_id) {
       return new Response(
       return new Response(
@@ -134,6 +134,9 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
     const shoprenterClientId = Deno.env.get('SHOPRENTER_APP_CLIENT_ID') || Deno.env.get('SHOPRENTER_CLIENT_ID')
     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 shoprenterClientSecret = Deno.env.get('SHOPRENTER_APP_CLIENT_SECRET') || Deno.env.get('SHOPRENTER_CLIENT_SECRET')
 
 
+    // Use phone_number_id from request body if provided, otherwise fall back to pending installation
+    const finalPhoneNumberId = phone_number_id || pendingInstall.phone_number_id
+
     // Create new store
     // Create new store
     const { data: newStore, error: createError } = await supabase
     const { data: newStore, error: createError } = await supabase
       .from('stores')
       .from('stores')
@@ -147,7 +150,7 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
         token_expires_at: new Date(Date.now() + (pendingInstall.expires_in || 3600) * 1000).toISOString(),
         token_expires_at: new Date(Date.now() + (pendingInstall.expires_in || 3600) * 1000).toISOString(),
         scopes: pendingInstall.scopes,
         scopes: pendingInstall.scopes,
         is_active: true,
         is_active: true,
-        phone_number_id: pendingInstall.phone_number_id,
+        phone_number: finalPhoneNumberId,
         alt_data: {
         alt_data: {
           client_id: shoprenterClientId,
           client_id: shoprenterClientId,
           client_secret: shoprenterClientSecret,
           client_secret: shoprenterClientSecret,
@@ -183,7 +186,7 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
     }
     }
 
 
     // Update phone number assignment if provided
     // Update phone number assignment if provided
-    if (pendingInstall.phone_number_id) {
+    if (finalPhoneNumberId) {
       const { error: phoneError } = await supabase
       const { error: phoneError } = await supabase
         .from('phone_numbers')
         .from('phone_numbers')
         .update({
         .update({
@@ -192,7 +195,7 @@ serve(wrapHandler('complete-shoprenter-install', async (req) => {
           assigned_at: new Date().toISOString(),
           assigned_at: new Date().toISOString(),
           is_available: false
           is_available: false
         })
         })
-        .eq('id', pendingInstall.phone_number_id)
+        .eq('id', finalPhoneNumberId)
 
 
       if (phoneError) {
       if (phoneError) {
         console.error('[ShopRenter] Error updating phone number:', phoneError)
         console.error('[ShopRenter] Error updating phone number:', phoneError)