import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useCart } from '../../context/useCart'; import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus, getProductById, getMediaUrl } from '../../api/api'; import type { CheckoutData, CryptoPaymentStatus } from '../../api/api'; import type { Product } from '../../api/api'; import Navbar from '../../components/Navbar'; import './Checkout.css'; // ============================================ // Interface pour les données du modal // ============================================ interface ConfirmationData { command_id: number; client_order_number?: number; assigned_to?: { username: string; distance_km?: number; eta_minutes?: number; }; queue_info?: { position: number; estimated_wait?: string; }; delivery_address: string; arrivalTime: string; total: number; referral_used?: number; clientInfo: { first_name: string; last_name: string; phone: string; delivery_address: string; }; } function Checkout() { const navigate = useNavigate(); const { cartItems, clearCart: clearCartContext, cartTotal } = useCart(); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); // État pour le modal de confirmation const [showConfirmation, setShowConfirmation] = useState(false); const [confirmationData, setConfirmationData] = useState(null); // État pour le modal zone non desservie const [showZoneModal, setShowZoneModal] = useState(false); const [zoneErrorMsg, setZoneErrorMsg] = useState(''); // Informations personnelles const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); const [address, setAddress] = useState(''); const [phone, setPhone] = useState(''); const total = cartTotal; // Images enrichies const [itemImages, setItemImages] = useState>({}); useEffect(() => { if (cartItems.length === 0) return; cartItems.forEach(async (item) => { try { const res = await getProductById(item.product_id); if (res.success && res.data) { const p: Product = res.data; const img = p.media?.find((m) => m && m.type === 'image'); if (img?.url) { setItemImages((prev) => ({ ...prev, [item.id]: getMediaUrl(img.url) })); } } } catch { /* ignore */ } }); }, [cartItems]); // Parrainage const [referralBalance, setReferralBalance] = useState(0); const [referralEnabled, setReferralEnabled] = useState(false); const [useReferral, setUseReferral] = useState(false); // Crypto const [cryptoEnabled, setCryptoEnabled] = useState(false); const [cryptoOnly, setCryptoOnly] = useState(false); const [cryptoCurrencies, setCryptoCurrencies] = useState([]); const [paymentMethod, setPaymentMethod] = useState<'especes' | 'crypto'>('especes'); const [payCurrency, setPayCurrency] = useState(''); const [cryptoPaymentData, setCryptoPaymentData] = useState(null); const [showCryptoModal, setShowCryptoModal] = useState(false); const [cryptoPolling, setCryptoPolling] = useState(false); const pollIntervalRef = useRef | null>(null); // ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION useEffect(() => { const checkAuth = () => { if (!isUserAuthenticated()) { console.log('❌ [Checkout] Utilisateur non authentifié, redirection vers /login/client'); navigate('/login/client', { replace: true }); } }; checkAuth(); }, [navigate]); // ✅ VÉRIFICATION CONTINUE (toutes les 5 secondes) useEffect(() => { const authInterval = setInterval(() => { if (!isUserAuthenticated()) { console.log('❌ [Checkout] Session expirée, redirection vers /login/client'); navigate('/login/client', { replace: true }); } }, 5000); return () => clearInterval(authInterval); }, [navigate]); // Charger les infos par défaut depuis le profil useEffect(() => { const savedAddress = localStorage.getItem('profile_default_address'); const savedPhone = localStorage.getItem('profile_default_phone'); if (savedAddress) setAddress(savedAddress); if (savedPhone) setPhone(savedPhone); // Pré-remplir nom/prénom depuis le backend getMyProfile().then((res) => { if (res.success && res.client) { if (res.client.nom) setLastName(res.client.nom); if (res.client.prenom) setFirstName(res.client.prenom); } }); }, []); // Charger settings publics (parrainage + crypto) useEffect(() => { getPublicSettings().then((settings) => { if (settings.referral_enabled) { setReferralEnabled(true); getReferralBalance().then((res) => { if (res.success) setReferralBalance(res.balance); }); } if (settings.crypto_payment_enabled && settings.nowpayments_currencies.length > 0) { setCryptoEnabled(true); setCryptoCurrencies(settings.nowpayments_currencies); setPayCurrency(settings.nowpayments_currencies[0]); if (settings.crypto_only) { setCryptoOnly(true); setPaymentMethod('crypto'); } } }); }, []); // Nettoyage du polling au démontage useEffect(() => { return () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }; }, []); /** * ✅ Récupérer le username du JWT */ const getUsername = (): string | null => { const username = extractUsernameFromToken(); if (!username) { console.warn('⚠️ Impossible d\'extraire le username du JWT'); return null; } console.log('✅ Username du JWT:', username); return username; }; /** * Calculer et formater l'heure d'arrivée estimée */ const calculateArrivalTime = (eta_minutes?: number, estimated_wait?: string): string => { const arrivalTime = new Date(); let totalMinutes = 0; if (eta_minutes) { totalMinutes = eta_minutes; } else if (estimated_wait) { const hourMatch = estimated_wait.match(/(\d+)\s*hour/i); const minuteMatch = estimated_wait.match(/(\d+)\s*minute/i); if (hourMatch) { totalMinutes += parseInt(hourMatch[1]) * 60; } if (minuteMatch) { totalMinutes += parseInt(minuteMatch[1]); } } arrivalTime.setMinutes(arrivalTime.getMinutes() + totalMinutes); return arrivalTime.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); }; const startCryptoPolling = (commandId: number) => { setCryptoPolling(true); pollIntervalRef.current = setInterval(async () => { const status = await getCryptoPaymentStatus(commandId); if (!status) return; setCryptoPaymentData(status); if (status.payment_status === 'finished' || status.payment_status === 'confirmed') { stopCryptoPolling(); const username = extractUsernameFromToken(); if (username) { await clearCart(username); await clearCartContext(); } } else if (status.payment_status === 'failed' || status.payment_status === 'expired') { stopCryptoPolling(); } }, 10000); }; const stopCryptoPolling = () => { setCryptoPolling(false); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }; const handleCryptoModalClose = () => { stopCryptoPolling(); setShowCryptoModal(false); navigate('/user/suivi-livraison'); }; /** * ✅ Gérer la soumission de la commande */ const handleSubmitOrder = async (e: React.FormEvent) => { e.preventDefault(); setError(''); // ✅ Vérifier l'auth avant de soumettre if (!isUserAuthenticated()) { console.log('❌ [handleSubmitOrder] Non authentifié'); navigate('/login/client', { replace: true }); return; } // Validation if (!firstName || !lastName || !address || !phone) { setError('Veuillez remplir tous les champs obligatoires'); return; } const username = getUsername(); if (!username) { setError('❌ Session expirée - Veuillez vous reconnecter'); navigate('/login/client', { replace: true }); return; } console.log('👤 Username du JWT:', username); setLoading(true); try { const checkoutData: CheckoutData = { username, delivery_address: address, first_name: firstName, last_name: lastName, phone, payment_method: paymentMethod === 'crypto' ? 'crypto' : 'especes', pay_currency: paymentMethod === 'crypto' ? payCurrency : undefined, use_referral_balance: useReferral && referralBalance > 0, }; console.log('📤 Envoi checkout avec JWT username:', checkoutData); const response = await createCheckout(checkoutData); console.log('📥 Réponse checkout:', response); // Paiement crypto : afficher le modal avec l'adresse wallet if (response.success && response.payment_method === 'crypto') { setCryptoPaymentData({ command_id: response.command_id!, client_order_number: (response as Record).client_order_number as number | undefined, payment_status: response.payment_status!, pay_address: response.pay_address!, pay_amount: response.pay_amount!, pay_currency: response.pay_currency!, price_amount: response.price_amount!, price_currency: response.price_currency!, }); setShowCryptoModal(true); startCryptoPolling(response.command_id); return; } if (!response.success && response.zone_error) { setZoneErrorMsg(response.message); setShowZoneModal(true); return; } if (response.success && response.command_id) { const { command_id, assigned_to, queue_info, delivery_address } = response; const frontendTotal = total; console.log(`💰 Total commande: ${frontendTotal.toFixed(2)}€`); // Calculer l'heure d'arrivée const arrivalTime = calculateArrivalTime( assigned_to?.eta_minutes, queue_info?.estimated_wait ); // ✅ Vider le panier AVANT d'afficher la confirmation console.log('🗑️ [CHECKOUT] Vidage du panier après commande réussie...'); try { const clearResponse = await clearCart(username); console.log('📥 [CHECKOUT] Réponse clearCart:', clearResponse); if (clearResponse.success) { await clearCartContext(); console.log('✅ [CHECKOUT] Panier vidé avec succès'); } else { console.warn('⚠️ [CHECKOUT] Erreur API clearCart (non bloquant):', clearResponse.message); await clearCartContext(); } } catch (clearErr) { console.warn('⚠️ [CHECKOUT] Erreur au vidage du panier (non bloquant):', clearErr); try { await clearCartContext(); } catch (localClearErr) { console.error('❌ [CHECKOUT] Impossible de vider le panier local:', localClearErr); } } // ✅ Préparer les données pour le modal setConfirmationData({ command_id, client_order_number: (response as Record).client_order_number as number | undefined, assigned_to, queue_info, delivery_address: delivery_address || address, arrivalTime, total: frontendTotal, referral_used: (response as Record).referral_used as number | undefined, clientInfo: { first_name: firstName, last_name: lastName, phone, delivery_address: address } }); // ✅ Afficher le modal setShowConfirmation(true); } else { setError(response.message || '❌ Erreur lors de la validation de la commande'); } } catch (err: unknown) { console.error('❌ Erreur checkout:', err); setError(err instanceof Error ? err.message : '❌ Erreur serveur. Veuillez réessayer.'); } finally { setLoading(false); } }; /** * ✅ Fermer le modal et rediriger */ const handleCloseConfirmation = () => { setShowConfirmation(false); setConfirmationData(null); navigate('/user/suivi-livraison'); }; return ( <>

Finaliser la commande

{/* Résumé de la commande */}

Résumé de la commande

{cartItems.map((item, index) => (
{itemImages[item.id] ? ( {item.name_product ) : (
)}

{item.name_product}

{item.quantity}x - {item.price.toFixed(2)} €

))}
Total: {total.toFixed(2)} €
{/* Formulaire */}
{error && (
⚠️ {error}
)}

Informations personnelles

setFirstName(e.target.value)} disabled={loading} required />
setLastName(e.target.value)} disabled={loading} required />
setAddress(e.target.value)} placeholder="Numéro, rue, ville, code postal" disabled={loading} required /> {localStorage.getItem('profile_default_address') && (

Pré-rempli depuis votre profil — modifiez si vous êtes ailleurs

)}
setPhone(e.target.value)} placeholder="+33 6 12 34 56 78" disabled={loading} required /> {localStorage.getItem('profile_default_phone') && (

Pré-rempli depuis votre profil

)}
{/* Méthode de paiement */} {cryptoEnabled && (

Méthode de paiement

{cryptoOnly ? (
Paiement uniquement en cryptomonnaie
) : (
)} {(paymentMethod === 'crypto') && (
{cryptoCurrencies.map((currency) => ( ))}

Vous recevrez l'adresse de paiement après validation

)}
)} {/* Toggle parrainage */} {referralEnabled && referralBalance > 0 && (

Solde parrainage

{referralBalance.toFixed(2)} € disponible

)}
Compte Telegram requis

Votre commande ne pourra être validée que si votre compte Telegram est lié. Rendez-vous dans votre profil pour le lier avant de confirmer.

{/* ============================================ */} {/* MODAL PAIEMENT CRYPTO */} {/* ============================================ */} {showCryptoModal && cryptoPaymentData && (

Paiement Crypto

Commande #{cryptoPaymentData.client_order_number ?? cryptoPaymentData.command_id}
Statut :{' '} {cryptoPaymentData.payment_status === 'waiting' && '⏳ En attente de paiement'} {cryptoPaymentData.payment_status === 'confirming' && '🔄 Confirmation en cours...'} {cryptoPaymentData.payment_status === 'confirmed' && '✅ Confirmé'} {cryptoPaymentData.payment_status === 'finished' && '✅ Paiement reçu !'} {cryptoPaymentData.payment_status === 'failed' && '❌ Paiement échoué'} {cryptoPaymentData.payment_status === 'expired' && '⌛ Expiré'} {!['waiting','confirming','confirmed','finished','failed','expired'].includes(cryptoPaymentData.payment_status) && cryptoPaymentData.payment_status}
Adresse de paiement
{cryptoPaymentData.pay_address}
Montant à envoyer
{cryptoPaymentData.pay_amount} {cryptoPaymentData.pay_currency.toUpperCase()} ≈ {cryptoPaymentData.price_amount.toFixed(2)} {cryptoPaymentData.price_currency.toUpperCase()}
{cryptoPolling && (
Vérification automatique toutes les 10 secondes...
)} {(cryptoPaymentData.payment_status === 'finished' || cryptoPaymentData.payment_status === 'confirmed') && (
Paiement confirmé ! Votre commande est en cours de traitement.
)}
)} {/* ============================================ */} {/* MODAL ZONE NON DESSERVIE */} {/* ============================================ */} {showZoneModal && (
setShowZoneModal(false)}>
e.stopPropagation()}>

Zone non desservie

{zoneErrorMsg}

Vérifiez l'adresse saisie ou contactez-nous pour connaître les zones de livraison disponibles.

)} {/* ============================================ */} {/* MODAL DE CONFIRMATION STYLISÉ */} {/* ============================================ */} {showConfirmation && confirmationData && (
e.stopPropagation()}> {/* Header */}

Commande Confirmée

{/* Body */}
{/* Numéro de commande */}
Détails de la commande
Numéro: #{confirmationData.client_order_number ?? confirmationData.command_id}
Date: {new Date().toLocaleString('fr-FR')}
{/* Informations client */}
Informations client
Nom: {confirmationData.clientInfo.first_name} {confirmationData.clientInfo.last_name}
Téléphone: {confirmationData.clientInfo.phone}
{/* Adresse de livraison */}
Adresse de livraison
{confirmationData.delivery_address}
{/* Parrainage utilisé */} {confirmationData.referral_used != null && confirmationData.referral_used > 0 && (
Parrainage appliqué
Crédit utilisé: -{confirmationData.referral_used.toFixed(2)} €
)} {/* Suivi */}
Suivi de livraison
Suivez votre livraison en temps réel depuis la page Suivi
{/* Total */}
TOTAL
{confirmationData.total.toFixed(2)} €
{/* Actions */}
)} ); } export default Checkout;