chore: update
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user