chore: update

This commit is contained in:
2026-03-19 19:56:51 +01:00
parent 3384ebded0
commit 7173266bb9
31 changed files with 3645 additions and 1942 deletions
+285 -7
View File
@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useCart } from '../../context/CartContext';
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings } from '../../api/api';
import type { CheckoutData } from '../../api/api';
import { useCart } from '../../context/useCart';
import { createCheckout, clearCart, extractUsernameFromToken, isUserAuthenticated, getReferralBalance, getPublicSettings, getMyProfile, getCryptoPaymentStatus } from '../../api/api';
import type { CheckoutData, CryptoPaymentStatus } from '../../api/api';
import Navbar from '../../components/Navbar';
import './Checkout.css';
@@ -42,6 +42,10 @@ function Checkout() {
const [showConfirmation, setShowConfirmation] = useState(false);
const [confirmationData, setConfirmationData] = useState<ConfirmationData | null>(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('');
@@ -55,6 +59,17 @@ function Checkout() {
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<string[]>([]);
const [paymentMethod, setPaymentMethod] = useState<'especes' | 'crypto'>('especes');
const [payCurrency, setPayCurrency] = useState('');
const [cryptoPaymentData, setCryptoPaymentData] = useState<CryptoPaymentStatus | null>(null);
const [showCryptoModal, setShowCryptoModal] = useState(false);
const [cryptoPolling, setCryptoPolling] = useState(false);
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// ✅ VÉRIFICATION INITIALE DE L'AUTHENTIFICATION
useEffect(() => {
const checkAuth = () => {
@@ -79,7 +94,22 @@ function Checkout() {
return () => clearInterval(authInterval);
}, [navigate]);
// Charger solde parrainage si activé
// 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 depuis le backend
getMyProfile().then((res) => {
if (res.success && res.client) {
if (res.client.nom) setLastName(res.client.nom);
}
});
}, []);
// Charger settings publics (parrainage + crypto)
useEffect(() => {
getPublicSettings().then((settings) => {
if (settings.referral_enabled) {
@@ -88,9 +118,25 @@ function Checkout() {
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
*/
@@ -135,6 +181,39 @@ function Checkout() {
});
};
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
*/
@@ -174,7 +253,8 @@ function Checkout() {
first_name: firstName,
last_name: lastName,
phone,
payment_method: 'especes',
payment_method: paymentMethod === 'crypto' ? 'crypto' : 'especes',
pay_currency: paymentMethod === 'crypto' ? payCurrency : undefined,
use_referral_balance: useReferral && referralBalance > 0,
};
@@ -183,6 +263,28 @@ function Checkout() {
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!,
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;
@@ -337,6 +439,9 @@ function Checkout() {
disabled={loading}
required
/>
{localStorage.getItem('profile_default_address') && (
<p className="form-prefill-hint"><i className="fas fa-map-marker-alt" /> Pré-rempli depuis votre profil modifiez si vous êtes ailleurs</p>
)}
</div>
<div className="form-group">
@@ -350,14 +455,71 @@ function Checkout() {
disabled={loading}
required
/>
{localStorage.getItem('profile_default_phone') && (
<p className="form-prefill-hint"><i className="fas fa-phone" /> Pré-rempli depuis votre profil</p>
)}
</div>
</div>
{/* Méthode de paiement */}
{cryptoEnabled && (
<div className="form-section">
<h3>Méthode de paiement</h3>
{cryptoOnly ? (
<div className="crypto-only-badge">
<i className="fas fa-coins" /> Paiement uniquement en cryptomonnaie
</div>
) : (
<div className="payment-method-selector">
<button
type="button"
className={`payment-method-btn ${paymentMethod === 'especes' ? 'active' : ''}`}
onClick={() => setPaymentMethod('especes')}
disabled={loading}
>
<i className="fas fa-money-bill-wave" /> Espèces
</button>
<button
type="button"
className={`payment-method-btn ${paymentMethod === 'crypto' ? 'active' : ''}`}
onClick={() => setPaymentMethod('crypto')}
disabled={loading}
>
<i className="fas fa-coins" /> Crypto
</button>
</div>
)}
{(paymentMethod === 'crypto') && (
<div className="crypto-currency-selector">
<label>Cryptomonnaie</label>
<div className="crypto-currency-grid">
{cryptoCurrencies.map((currency) => (
<button
key={currency}
type="button"
className={`crypto-currency-btn ${payCurrency === currency ? 'active' : ''}`}
onClick={() => setPayCurrency(currency)}
disabled={loading}
>
{currency.toUpperCase()}
</button>
))}
</div>
<p className="crypto-info-hint">
<i className="fas fa-info-circle" /> Vous recevrez l'adresse de paiement après validation
</p>
</div>
)}
</div>
)}
{/* Toggle parrainage */}
{referralEnabled && referralBalance > 0 && (
<div className="referral-toggle-box">
<div className="referral-toggle-info">
<span className="referral-toggle-icon">🎁</span>
<span className="referral-toggle-icon"><i className="fas fa-gift"></i></span>
<div>
<p className="referral-toggle-label">Solde parrainage</p>
<p className="referral-toggle-balance">{referralBalance.toFixed(2)} € disponible</p>
@@ -397,6 +559,111 @@ function Checkout() {
</div>
</div>
{/* ============================================ */}
{/* MODAL PAIEMENT CRYPTO */}
{/* ============================================ */}
{showCryptoModal && cryptoPaymentData && (
<div className="confirmation-modal-overlay">
<div className="confirmation-modal">
<div className="confirmation-modal-header" style={{ background: 'linear-gradient(135deg, #f7931a, #c2620a)' }}>
<i className="fas fa-coins confirmation-icon" />
<h2>Paiement Crypto</h2>
</div>
<div className="confirmation-modal-body">
<div className="confirmation-section">
<div className="confirmation-section-title">
<i className="fas fa-receipt icon" /> Commande #{cryptoPaymentData.command_id}
</div>
<div className="confirmation-detail">
<strong>Statut :</strong>{' '}
<span className={`crypto-status crypto-status--${cryptoPaymentData.payment_status}`}>
{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}
</span>
</div>
</div>
<div className="confirmation-section">
<div className="confirmation-section-title">
<i className="fas fa-wallet icon" /> Adresse de paiement
</div>
<div className="crypto-address-box">
<code className="crypto-address">{cryptoPaymentData.pay_address}</code>
<button
type="button"
className="crypto-copy-btn"
onClick={() => navigator.clipboard.writeText(cryptoPaymentData.pay_address)}
>
<i className="fas fa-copy" /> Copier
</button>
</div>
</div>
<div className="confirmation-section">
<div className="confirmation-section-title">
<i className="fas fa-coins icon" /> Montant à envoyer
</div>
<div className="confirmation-detail">
<strong>{cryptoPaymentData.pay_amount} {cryptoPaymentData.pay_currency.toUpperCase()}</strong>
<span className="crypto-equiv"> ≈ {cryptoPaymentData.price_amount.toFixed(2)} {cryptoPaymentData.price_currency.toUpperCase()}</span>
</div>
</div>
{cryptoPolling && (
<div className="crypto-polling-info">
<i className="fas fa-spinner fa-spin" /> Vérification automatique toutes les 10 secondes...
</div>
)}
{(cryptoPaymentData.payment_status === 'finished' || cryptoPaymentData.payment_status === 'confirmed') && (
<div className="crypto-success-msg">
<i className="fas fa-check-circle" /> Paiement confirmé ! Votre commande est en cours de traitement.
</div>
)}
</div>
<div className="confirmation-modal-actions">
<button className="confirmation-button" style={{ background: '#374151' }} onClick={handleCryptoModalClose}>
<i className="fas fa-location-arrow" /> Suivre ma commande
</button>
</div>
</div>
</div>
)}
{/* ============================================ */}
{/* MODAL ZONE NON DESSERVIE */}
{/* ============================================ */}
{showZoneModal && (
<div className="confirmation-modal-overlay" onClick={() => setShowZoneModal(false)}>
<div className="confirmation-modal" onClick={(e) => e.stopPropagation()}>
<div className="confirmation-modal-header" style={{ background: 'linear-gradient(135deg, #dc2626, #991b1b)' }}>
<i className="fas fa-map-marker-alt confirmation-icon"></i>
<h2>Zone non desservie</h2>
</div>
<div className="confirmation-modal-body">
<div className="confirmation-section">
<div className="confirmation-detail" style={{ textAlign: 'center', padding: '1rem 0' }}>
<p style={{ fontSize: '1rem', marginBottom: '0.75rem' }}>{zoneErrorMsg}</p>
<p style={{ color: '#9ca3af', fontSize: '0.875rem' }}>
Vérifiez l'adresse saisie ou contactez-nous pour connaître les zones de livraison disponibles.
</p>
</div>
</div>
</div>
<div className="confirmation-modal-actions">
<button className="confirmation-button" style={{ background: '#374151' }} onClick={() => setShowZoneModal(false)}>
<i className="fas fa-arrow-left"></i> Modifier l'adresse
</button>
</div>
</div>
</div>
)}
{/* ============================================ */}
{/* MODAL DE CONFIRMATION STYLISÉ */}
{/* ============================================ */}
@@ -463,6 +730,17 @@ function Checkout() {
</div>
)}
{/* Suivi */}
<div className="confirmation-section">
<div className="confirmation-section-title">
<i className="fas fa-map-marker-alt icon"></i>
Suivi de livraison
</div>
<div className="confirmation-detail">
Suivez votre livraison en temps réel depuis la page Suivi
</div>
</div>
{/* Total */}
<div className="confirmation-total">
<div className="confirmation-total-label">