Spaces:
Sleeping
Sleeping
| // File: src/components/VerificationModal.js | |
| import React, { useState, useEffect } from 'react'; | |
| import { supabase } from '../supabaseClient'; | |
| import { motion } from 'framer-motion'; | |
| export default function VerificationModal({ onClose, onVerified }) { | |
| const [step, setStep] = useState(0); // 0 = Loading Profile, 1 = Confirm Send, 2 = Enter OTP | |
| const [phone, setPhone] = useState(''); | |
| const [otp, setOtp] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [error, setError] = useState(null); | |
| // 1. Fetch User's Phone on Mount | |
| useEffect(() => { | |
| const fetchUserProfile = async () => { | |
| setError(null); | |
| try { | |
| const { data: { user } } = await supabase.auth.getUser(); | |
| if (!user) throw new Error("No user found."); | |
| const { data: profile, error: profileError } = await supabase | |
| .from('profiles') | |
| .select('phone') | |
| .eq('id', user.id) | |
| .single(); | |
| if (profileError) throw profileError; | |
| if (!profile.phone) { | |
| setError("No phone number found in your profile. Please go to your Profile settings and add a phone number first."); | |
| return; | |
| } | |
| setPhone(profile.phone); | |
| setStep(1); // Ready to send | |
| } catch (err) { | |
| console.error("Profile Fetch Error:", err); | |
| setError("Could not load profile details."); | |
| } | |
| }; | |
| fetchUserProfile(); | |
| }, []); | |
| // 2. Helper to mask phone number (e.g., +1 ********99) | |
| const maskPhone = (p) => { | |
| if (!p || p.length < 5) return p; | |
| return `${p.slice(0, 3)}****${p.slice(-3)}`; | |
| }; | |
| // 3. Send OTP | |
| const handleSendOtp = async () => { | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| // Call the 'otp' Edge Function with action: 'send' | |
| const { data, error } = await supabase.functions.invoke('otp', { | |
| body: { action: 'send' } | |
| }); | |
| if (error) throw error; | |
| // Check for logic error from function | |
| if (data?.error) throw new Error(data.error); | |
| // For testing only (Remove in production) | |
| if (data?.dev_otp) console.log("DEV OTP:", data.dev_otp); | |
| setStep(2); | |
| } catch (err) { | |
| setError(err.message || "Failed to send OTP"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| // 4. Verify OTP | |
| const handleVerifyOtp = async () => { | |
| if (!otp) return setError("Please enter the code."); | |
| setLoading(true); | |
| setError(null); | |
| try { | |
| // Call the 'otp' Edge Function with action: 'verify' | |
| const { data, error } = await supabase.functions.invoke('otp', { | |
| body: { | |
| action: 'verify', | |
| userCode: otp | |
| } | |
| }); | |
| if (error) throw error; | |
| if (data?.error) throw new Error(data.error); | |
| // Success! | |
| onVerified(); | |
| } catch (err) { | |
| setError(err.message || "Invalid OTP"); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| return ( | |
| <div style={{ position: 'fixed', inset: 0, backgroundColor: 'rgba(0,0,0,0.8)', display: 'grid', placeItems: 'center', zIndex: 100 }}> | |
| <motion.div | |
| initial={{ scale: 0.9, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| style={{ backgroundColor: '#111827', padding: '2rem', borderRadius: '1rem', width: '100%', maxWidth: '400px', border: '1px solid #374151' }} | |
| > | |
| {/* --- HEADER --- */} | |
| <div style={{ textAlign: 'center', marginBottom: '1.5rem' }}> | |
| <div style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '3rem', height: '3rem', borderRadius: '50%', backgroundColor: 'rgba(245, 158, 11, 0.2)', color: '#f59e0b', marginBottom: '1rem' }}> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg> | |
| </div> | |
| <h2 style={{ fontSize: '1.5rem', fontWeight: 'bold', color: 'white' }}> | |
| {step === 2 ? 'Enter Code' : 'Verify Phone Number'} | |
| </h2> | |
| </div> | |
| {/* --- ERROR MESSAGE --- */} | |
| {error && ( | |
| <div style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', border: '1px solid rgba(239, 68, 68, 0.2)', padding: '0.75rem', borderRadius: '0.5rem', color: '#f87171', marginBottom: '1.5rem', fontSize: '0.875rem', textAlign: 'center' }}> | |
| {error} | |
| </div> | |
| )} | |
| {/* --- STEP 0: LOADING --- */} | |
| {step === 0 && !error && ( | |
| <p style={{ textAlign: 'center', color: '#9ca3af' }}>Fetching contact details...</p> | |
| )} | |
| {/* --- STEP 1: CONFIRM PHONE --- */} | |
| {step === 1 && ( | |
| <> | |
| <div style={{ textAlign: 'center', marginBottom: '1.5rem', backgroundColor: '#1f2937', padding: '1rem', borderRadius: '0.5rem', border: '1px solid #374151' }}> | |
| <p style={{ color: '#9ca3af', fontSize: '0.875rem', marginBottom: '0.5rem' }}>We will send a code to:</p> | |
| <p style={{ color: 'white', fontSize: '1.25rem', fontWeight: 'bold', letterSpacing: '0.05em' }}> | |
| {maskPhone(phone)} | |
| </p> | |
| </div> | |
| <button | |
| onClick={handleSendOtp} | |
| disabled={loading} | |
| style={{ width: '100%', padding: '0.75rem', backgroundColor: '#f59e0b', color: 'black', borderRadius: '0.5rem', fontWeight: 'bold', border: 'none', cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }} | |
| > | |
| {loading ? 'Sending...' : 'Send Verification Code'} | |
| </button> | |
| </> | |
| )} | |
| {/* --- STEP 2: ENTER OTP --- */} | |
| {step === 2 && ( | |
| <> | |
| <p style={{ color: '#9ca3af', textAlign: 'center', marginBottom: '1.5rem', fontSize: '0.875rem' }}> | |
| Enter the 6-digit code sent to your phone. | |
| </p> | |
| <div style={{ marginBottom: '1.5rem' }}> | |
| <input | |
| type="text" | |
| placeholder="000000" | |
| maxLength={6} | |
| value={otp} | |
| onChange={(e) => setOtp(e.target.value)} | |
| style={{ width: '100%', padding: '0.75rem', borderRadius: '0.5rem', backgroundColor: '#374151', color: 'white', border: '1px solid #4b5563', outline: 'none', textAlign: 'center', letterSpacing: '0.5em', fontSize: '1.5rem', fontWeight: 'bold' }} | |
| /> | |
| </div> | |
| <button | |
| onClick={handleVerifyOtp} | |
| disabled={loading} | |
| style={{ width: '100%', padding: '0.75rem', backgroundColor: '#10b981', color: 'white', borderRadius: '0.5rem', fontWeight: 'bold', border: 'none', cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }} | |
| > | |
| {loading ? 'Verifying...' : 'Verify & Apply'} | |
| </button> | |
| </> | |
| )} | |
| <button onClick={onClose} style={{ marginTop: '1.5rem', width: '100%', padding: '0.5rem', color: '#6b7280', background: 'none', border: 'none', cursor: 'pointer', fontSize: '0.875rem' }}>Cancel</button> | |
| </motion.div> | |
| </div> | |
| ); | |
| } |