Browse Source

feat: implement phone numbers with country-based filtering #60

- Created phone_numbers table with country-based access control
- Added country_code and country_name fields to profiles table
- Populated database with 10 fake Hungarian test numbers
- Implemented RLS policies for country-specific phone number filtering
- Added GET /api/phone-numbers endpoint to fetch numbers by user's country
- Added PUT /api/profile/country endpoint to update user's country
- Updated OnboardingContent.tsx to fetch phone numbers from database
- Added loading states and error handling in UI
- Phone numbers now dynamically filtered based on user's country

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

Co-Authored-By: Claude <noreply@anthropic.com>
Claude 5 months ago
parent
commit
c38e81cacb
2 changed files with 231 additions and 51 deletions
  1. 127 51
      shopcall.ai-main/src/components/OnboardingContent.tsx
  2. 104 0
      supabase/functions/api/index.ts

+ 127 - 51
shopcall.ai-main/src/components/OnboardingContent.tsx

@@ -1,32 +1,35 @@
 
 
-import { useState } from "react";
+import { useState, useEffect } from "react";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
 import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
 import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
-import { 
-  Store, 
-  PhoneCall, 
-  CreditCard, 
-  Check, 
-  ArrowRight, 
+import {
+  Store,
+  PhoneCall,
+  CreditCard,
+  Check,
+  ArrowRight,
   ArrowLeft,
   ArrowLeft,
   Zap,
   Zap,
   Crown,
   Crown,
   Star,
   Star,
   Shield,
   Shield,
-  Globe
+  Globe,
+  Loader2
 } from "lucide-react";
 } from "lucide-react";
 
 
-const phoneNumbers = [
-  { number: "+1 (555) 123-4567", location: "New York, US", type: "Local" },
-  { number: "+1 (555) 987-6543", location: "Los Angeles, US", type: "Local" },
-  { number: "+1 (800) 555-0123", location: "Toll-free US", type: "Toll-free" },
-  { number: "+44 20 7946 0958", location: "London, UK", type: "Local" },
-  { number: "+1 (416) 555-7890", location: "Toronto, CA", type: "Local" },
-];
+interface PhoneNumber {
+  id: string;
+  phone_number: string;
+  country_code: string;
+  country_name: string;
+  location: string | null;
+  phone_type: string;
+  is_available: boolean;
+}
 
 
 const packages = [
 const packages = [
   {
   {
@@ -84,6 +87,56 @@ export function OnboardingContent() {
   const [shopifyUrl, setShopifyUrl] = useState("");
   const [shopifyUrl, setShopifyUrl] = useState("");
   const [selectedPhone, setSelectedPhone] = useState("");
   const [selectedPhone, setSelectedPhone] = useState("");
   const [selectedPackage, setSelectedPackage] = useState("free-trial");
   const [selectedPackage, setSelectedPackage] = useState("free-trial");
+  const [phoneNumbers, setPhoneNumbers] = useState<PhoneNumber[]>([]);
+  const [loadingPhoneNumbers, setLoadingPhoneNumbers] = useState(false);
+  const [phoneNumbersError, setPhoneNumbersError] = useState<string | null>(null);
+
+  // Fetch phone numbers when component mounts or step changes to 2
+  useEffect(() => {
+    if (currentStep === 2 && phoneNumbers.length === 0) {
+      fetchPhoneNumbers();
+    }
+  }, [currentStep]);
+
+  const fetchPhoneNumbers = async () => {
+    setLoadingPhoneNumbers(true);
+    setPhoneNumbersError(null);
+
+    try {
+      const sessionData = localStorage.getItem('session_data');
+      if (!sessionData) {
+        setPhoneNumbersError('Not authenticated');
+        return;
+      }
+
+      const session = JSON.parse(sessionData);
+      const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:54321/functions/v1';
+
+      const response = await fetch(`${apiUrl}/api/phone-numbers`, {
+        headers: {
+          'Authorization': `Bearer ${session.access_token}`,
+          'Content-Type': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error('Failed to fetch phone numbers');
+      }
+
+      const data = await response.json();
+
+      if (data.success && data.phone_numbers) {
+        setPhoneNumbers(data.phone_numbers);
+      } else {
+        setPhoneNumbersError('No phone numbers available');
+      }
+    } catch (error) {
+      console.error('Error fetching phone numbers:', error);
+      setPhoneNumbersError('Failed to load phone numbers');
+    } finally {
+      setLoadingPhoneNumbers(false);
+    }
+  };
 
 
   const nextStep = () => {
   const nextStep = () => {
     if (currentStep < 3) {
     if (currentStep < 3) {
@@ -198,45 +251,68 @@ export function OnboardingContent() {
             {/* Step 2: Phone Number Selection */}
             {/* Step 2: Phone Number Selection */}
             {currentStep === 2 && (
             {currentStep === 2 && (
               <div className="space-y-6">
               <div className="space-y-6">
-                <RadioGroup value={selectedPhone} onValueChange={setSelectedPhone}>
-                  <div className="grid gap-4">
-                    {phoneNumbers.map((phone) => (
-                      <div key={phone.number} className="flex items-center space-x-3">
-                        <RadioGroupItem value={phone.number} id={phone.number} />
-                        <Label 
-                          htmlFor={phone.number} 
-                          className="flex-1 cursor-pointer"
-                        >
-                          <Card className="bg-slate-700 border-slate-600 hover:border-cyan-500 transition-colors">
-                            <CardContent className="p-4">
-                              <div className="flex items-center justify-between">
-                                <div>
-                                  <div className="text-white font-mono text-lg">{phone.number}</div>
-                                  <div className="text-slate-400 text-sm flex items-center gap-2">
-                                    <Globe className="w-4 h-4" />
-                                    {phone.location}
+                {loadingPhoneNumbers ? (
+                  <div className="flex items-center justify-center py-8">
+                    <Loader2 className="w-8 h-8 text-cyan-500 animate-spin" />
+                    <span className="ml-3 text-slate-400">Loading phone numbers...</span>
+                  </div>
+                ) : phoneNumbersError ? (
+                  <div className="bg-red-500/10 border border-red-500 rounded-lg p-4">
+                    <p className="text-red-500">{phoneNumbersError}</p>
+                    <Button
+                      onClick={fetchPhoneNumbers}
+                      className="mt-4 bg-cyan-500 hover:bg-cyan-600"
+                    >
+                      Retry
+                    </Button>
+                  </div>
+                ) : phoneNumbers.length === 0 ? (
+                  <div className="bg-yellow-500/10 border border-yellow-500 rounded-lg p-4">
+                    <p className="text-yellow-500">No phone numbers available for your country yet. Please contact support.</p>
+                  </div>
+                ) : (
+                  <>
+                    <RadioGroup value={selectedPhone} onValueChange={setSelectedPhone}>
+                      <div className="grid gap-4">
+                        {phoneNumbers.map((phone) => (
+                          <div key={phone.id} className="flex items-center space-x-3">
+                            <RadioGroupItem value={phone.phone_number} id={phone.id} />
+                            <Label
+                              htmlFor={phone.id}
+                              className="flex-1 cursor-pointer"
+                            >
+                              <Card className="bg-slate-700 border-slate-600 hover:border-cyan-500 transition-colors">
+                                <CardContent className="p-4">
+                                  <div className="flex items-center justify-between">
+                                    <div>
+                                      <div className="text-white font-mono text-lg">{phone.phone_number}</div>
+                                      <div className="text-slate-400 text-sm flex items-center gap-2">
+                                        <Globe className="w-4 h-4" />
+                                        {phone.location || phone.country_name}
+                                      </div>
+                                    </div>
+                                    <Badge
+                                      className={phone.phone_type === "toll-free" ? "bg-purple-500" : "bg-green-500"}
+                                    >
+                                      {phone.phone_type}
+                                    </Badge>
                                   </div>
                                   </div>
-                                </div>
-                                <Badge 
-                                  className={phone.type === "Toll-free" ? "bg-purple-500" : "bg-green-500"}
-                                >
-                                  {phone.type}
-                                </Badge>
-                              </div>
-                            </CardContent>
-                          </Card>
-                        </Label>
+                                </CardContent>
+                              </Card>
+                            </Label>
+                          </div>
+                        ))}
                       </div>
                       </div>
-                    ))}
-                  </div>
-                </RadioGroup>
+                    </RadioGroup>
 
 
-                <div className="bg-slate-700/50 p-4 rounded-lg">
-                  <p className="text-slate-300 text-sm">
-                    <strong>Note:</strong> Your selected phone number will be instantly activated and ready to receive calls. 
-                    You can always add more numbers later from your dashboard.
-                  </p>
-                </div>
+                    <div className="bg-slate-700/50 p-4 rounded-lg">
+                      <p className="text-slate-300 text-sm">
+                        <strong>Note:</strong> Your selected phone number will be instantly activated and ready to receive calls.
+                        You can always add more numbers later from your dashboard.
+                      </p>
+                    </div>
+                  </>
+                )}
               </div>
               </div>
             )}
             )}
 
 

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

@@ -1564,6 +1564,110 @@ serve(wrapHandler('api', async (req) => {
       )
       )
     }
     }
 
 
+    // GET /api/phone-numbers - Get available phone numbers for user's country
+    if (path === 'phone-numbers' && req.method === 'GET') {
+      try {
+        // Get user's profile to find their country
+        const { data: profile, error: profileError } = await supabase
+          .from('profiles')
+          .select('country_code, country_name')
+          .eq('id', user.id)
+          .single()
+
+        if (profileError) {
+          console.error('Error fetching user profile:', profileError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch user profile' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // If user doesn't have a country set, return all available numbers
+        let query = supabase
+          .from('phone_numbers')
+          .select('*')
+          .eq('is_active', true)
+          .order('country_name', { ascending: true })
+          .order('location', { ascending: true })
+
+        // Filter by user's country if set
+        if (profile?.country_code) {
+          query = query.eq('country_code', profile.country_code)
+        }
+
+        const { data: phoneNumbers, error: phoneError } = await query
+
+        if (phoneError) {
+          console.error('Error fetching phone numbers:', phoneError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to fetch phone numbers' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            phone_numbers: phoneNumbers || [],
+            user_country: profile?.country_code || null
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in phone-numbers endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/profile/country - Update user's country
+    if (path === 'profile/country' && req.method === 'PUT') {
+      try {
+        const { country_code, country_name } = await req.json()
+
+        if (!country_code || !country_name) {
+          return new Response(
+            JSON.stringify({ error: 'country_code and country_name are required' }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        // Update user's profile with country information
+        const { error: updateError } = await supabase
+          .from('profiles')
+          .update({
+            country_code,
+            country_name,
+            updated_at: new Date().toISOString()
+          })
+          .eq('id', user.id)
+
+        if (updateError) {
+          console.error('Error updating user country:', updateError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update country' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: 'Country updated successfully'
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in profile/country endpoint:', error)
+        return new Response(
+          JSON.stringify({ error: 'Internal server error' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
     return new Response(
     return new Response(
       JSON.stringify({ error: 'Not found' }),
       JSON.stringify({ error: 'Not found' }),
       { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
       { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }