fix: telegram link

This commit is contained in:
2026-05-12 21:39:59 +02:00
parent d13a980447
commit ee0abcd223
+419 -293
View File
@@ -1,307 +1,433 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Navbar from '../../components/Navbar';
import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram } from '../../api/api';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Navbar from "../../components/Navbar";
import {
faUser, faMapMarkerAlt, faPhone, faCommentDots,
faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock,
} from '@fortawesome/free-solid-svg-icons';
import './ProfilePage.css';
isUserAuthenticated,
extractUsernameFromToken,
getMyProfile,
updateMyProfile,
getTelegramStatus,
generateTelegramLinkToken,
unlinkTelegram,
} from "../../api/api";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faUser,
faMapMarkerAlt,
faPhone,
faCommentDots,
faSave,
faCheckCircle,
faExclamationTriangle,
faPaperPlane,
faUnlink,
faTimes,
faLock,
} from "@fortawesome/free-solid-svg-icons";
import "./ProfilePage.css";
const STORAGE_ADDRESS = 'profile_default_address';
const STORAGE_PHONE = 'profile_default_phone';
const STORAGE_SIGNAL = 'profile_signal_pseudo';
const STORAGE_ADDRESS = "profile_default_address";
const STORAGE_PHONE = "profile_default_phone";
const STORAGE_SIGNAL = "profile_signal_pseudo";
export default function ProfilePage() {
const navigate = useNavigate();
const navigate = useNavigate();
// Données compte (backend)
const [nom, setNom] = useState('');
const [prenom, setPrenom] = useState('');
const [telephone, setTelephone] = useState('');
const [loadingProfile, setLoadingProfile] = useState(true);
// Données compte (backend)
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [telephone, setTelephone] = useState("");
const [loadingProfile, setLoadingProfile] = useState(true);
// Données locales (localStorage)
const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? '');
const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? '');
const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? '');
// Feedback
const [savingContact, setSavingContact] = useState(false);
const [successMsg, setSuccessMsg] = useState('');
const [errorMsg, setErrorMsg] = useState('');
// Telegram
const [tgLinked, setTgLinked] = useState(false);
const [tgEnabled, setTgEnabled] = useState(false);
const [tgLoading, setTgLoading] = useState(false);
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
const username = extractUsernameFromToken() ?? '';
useEffect(() => {
if (!isUserAuthenticated()) {
navigate('/login/client', { replace: true });
return;
}
// Statut Telegram
getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
// Charger depuis backend
getMyProfile().then((res) => {
if (res.success && res.client) {
setNom(res.client.nom ?? '');
setPrenom(res.client.prenom ?? '');
setTelephone(res.client.telephone ?? '');
// Initialiser le téléphone par défaut si pas encore défini
if (!localStorage.getItem(STORAGE_PHONE) && res.client.telephone) {
setDefaultPhone(res.client.telephone);
}
}
setLoadingProfile(false);
});
}, [navigate]);
const showSuccess = (msg: string) => {
setSuccessMsg(msg);
setErrorMsg('');
setTimeout(() => setSuccessMsg(''), 3000);
};
const showError = (msg: string) => {
setErrorMsg(msg);
setSuccessMsg('');
setTimeout(() => setErrorMsg(''), 4000);
};
const saveLocal = () => {
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
setShowSaveModal(false);
showSuccess('Informations par défaut enregistrées');
};
const handleLinkTelegram = async () => {
setTgLoading(true);
const res = await generateTelegramLinkToken();
setTgLoading(false);
if (res.error || !res.link_url) {
showError(res.error || 'Service Telegram non disponible');
return;
}
window.open(res.link_url, '_blank');
};
const handleUnlinkTelegram = async () => {
if (!window.confirm('Délier votre compte Telegram ? Vous ne recevrez plus de notifications.')) return;
await unlinkTelegram();
setTgLinked(false);
showSuccess('Compte Telegram délié');
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
setSavingContact(false);
if (res.success) {
showSuccess('Profil mis à jour');
} else {
showError(res.message ?? 'Erreur lors de la mise à jour');
}
};
if (loadingProfile) {
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-loading"><div className="spinner" /></div>
</div>
</>
// Données locales (localStorage)
const [defaultAddress, setDefaultAddress] = useState(
() => localStorage.getItem(STORAGE_ADDRESS) ?? "",
);
const [defaultPhone, setDefaultPhone] = useState(
() => localStorage.getItem(STORAGE_PHONE) ?? "",
);
const [signalPseudo, setSignalPseudo] = useState(
() => localStorage.getItem(STORAGE_SIGNAL) ?? "",
);
}
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-header">
<div className="profile-avatar">
<FontAwesomeIcon icon={faUser} />
</div>
<div>
<h1 className="profile-title">Mon Profil</h1>
<p className="profile-username">@{username}</p>
</div>
</div>
// Feedback
const [savingContact, setSavingContact] = useState(false);
const [successMsg, setSuccessMsg] = useState("");
const [errorMsg, setErrorMsg] = useState("");
{successMsg && (
<div className="profile-alert profile-alert--success">
<FontAwesomeIcon icon={faCheckCircle} /> {successMsg}
</div>
)}
{errorMsg && (
<div className="profile-alert profile-alert--error">
<FontAwesomeIcon icon={faExclamationTriangle} /> {errorMsg}
</div>
)}
// Telegram
const [tgLinked, setTgLinked] = useState(false);
const [tgEnabled, setTgEnabled] = useState(false);
const [tgLoading, setTgLoading] = useState(false);
{/* Section compte */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faUser} className="profile-card-icon" />
Mon compte
</h2>
<div className="profile-fields">
<div className="profile-row">
<div className="profile-group">
<label>Prénom</label>
<input
type="text"
value={prenom}
onChange={(e) => setPrenom(e.target.value)}
placeholder="Votre prénom"
/>
</div>
<div className="profile-group">
<label>Nom</label>
<input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom"
/>
</div>
// Modal confirmation infos par défaut
const [showSaveModal, setShowSaveModal] = useState(false);
const username = extractUsernameFromToken() ?? "";
useEffect(() => {
if (!isUserAuthenticated()) {
navigate("/login/client", { replace: true });
return;
}
// Statut Telegram
getTelegramStatus().then((s) => {
setTgLinked(s.linked);
setTgEnabled(s.enabled);
});
// Charger depuis backend
getMyProfile().then((res) => {
if (res.success && res.client) {
setNom(res.client.nom ?? "");
setPrenom(res.client.prenom ?? "");
setTelephone(res.client.telephone ?? "");
// Initialiser le téléphone par défaut si pas encore défini
if (
!localStorage.getItem(STORAGE_PHONE) &&
res.client.telephone
) {
setDefaultPhone(res.client.telephone);
}
}
setLoadingProfile(false);
});
}, [navigate]);
const showSuccess = (msg: string) => {
setSuccessMsg(msg);
setErrorMsg("");
setTimeout(() => setSuccessMsg(""), 3000);
};
const showError = (msg: string) => {
setErrorMsg(msg);
setSuccessMsg("");
setTimeout(() => setErrorMsg(""), 4000);
};
const saveLocal = () => {
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
setShowSaveModal(false);
showSuccess("Informations par défaut enregistrées");
};
const handleLinkTelegram = async () => {
setTgLoading(true);
// ✅ Ouvrir AVANT le await
const newWindow = window.open("", "_blank");
const res = await generateTelegramLinkToken();
setTgLoading(false);
if (res.error || !res.link_url) {
newWindow?.close();
showError(res.error || "Service Telegram non disponible");
return;
}
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
if (isIOS) {
const botUsername = res.link_url.split("t.me/")[1]?.split("?")[0];
const token = new URL(res.link_url).searchParams.get("start");
newWindow!.location.href = `tg://resolve?domain=${botUsername}&start=${token}`;
setTimeout(() => {
newWindow!.location.href = res.link_url!;
}, 1500);
} else {
newWindow!.location.href = res.link_url;
}
};
const handleUnlinkTelegram = async () => {
if (
!window.confirm(
"Délier votre compte Telegram ? Vous ne recevrez plus de notifications.",
)
)
return;
await unlinkTelegram();
setTgLinked(false);
showSuccess("Compte Telegram délié");
};
const saveContact = async () => {
setSavingContact(true);
const res = await updateMyProfile({
nom: nom.trim(),
prenom: prenom.trim(),
telephone: telephone.trim(),
});
setSavingContact(false);
if (res.success) {
showSuccess("Profil mis à jour");
} else {
showError(res.message ?? "Erreur lors de la mise à jour");
}
};
if (loadingProfile) {
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-loading">
<div className="spinner" />
</div>
</div>
</>
);
}
return (
<>
<Navbar />
<div className="profile-container">
<div className="profile-header">
<div className="profile-avatar">
<FontAwesomeIcon icon={faUser} />
</div>
<div>
<h1 className="profile-title">Mon Profil</h1>
<p className="profile-username">@{username}</p>
</div>
</div>
{successMsg && (
<div className="profile-alert profile-alert--success">
<FontAwesomeIcon icon={faCheckCircle} /> {successMsg}
</div>
)}
{errorMsg && (
<div className="profile-alert profile-alert--error">
<FontAwesomeIcon icon={faExclamationTriangle} />{" "}
{errorMsg}
</div>
)}
{/* Section compte */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faUser}
className="profile-card-icon"
/>
Mon compte
</h2>
<div className="profile-fields">
<div className="profile-row">
<div className="profile-group">
<label>Prénom</label>
<input
type="text"
value={prenom}
onChange={(e) => setPrenom(e.target.value)}
placeholder="Votre prénom"
/>
</div>
<div className="profile-group">
<label>Nom</label>
<input
type="text"
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Votre nom"
/>
</div>
</div>
<div className="profile-group">
<label>Téléphone (compte)</label>
<input
type="tel"
value={telephone}
onChange={(e) => setTelephone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
</div>
<button
className="profile-btn"
onClick={saveContact}
disabled={savingContact}
>
<FontAwesomeIcon icon={faSave} />
{savingContact
? " Enregistrement..."
: " Enregistrer le compte"}
</button>
<button
className="profile-btn profile-btn--secondary"
onClick={() => navigate("/user/change-password")}
style={{ marginTop: "0.6rem" }}
>
<FontAwesomeIcon icon={faLock} /> Changer le mot de
passe
</button>
</div>
{/* Section adresse par défaut */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faMapMarkerAlt}
className="profile-card-icon profile-card-icon--address"
/>
Adresse par défaut
</h2>
<p className="profile-hint">
Sera pré-remplie dans le formulaire de commande. Vous
pourrez la modifier si vous n'êtes pas à cette adresse.
</p>
<div className="profile-group">
<label>Adresse</label>
<input
type="text"
value={defaultAddress}
onChange={(e) => setDefaultAddress(e.target.value)}
placeholder="Numéro, rue, ville, code postal"
/>
</div>
</div>
{/* Section contact commande */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faPhone}
className="profile-card-icon profile-card-icon--phone"
/>
Contact livraison
</h2>
<p className="profile-hint">
Numéro utilisé par le livreur lors de la livraison. Peut
être différent du numéro de votre compte.
</p>
<div className="profile-group">
<label>Téléphone par défaut</label>
<input
type="tel"
value={defaultPhone}
onChange={(e) => setDefaultPhone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
<div
className="profile-group"
style={{ marginTop: "1rem" }}
>
<label>
<FontAwesomeIcon
icon={faCommentDots}
style={{ marginRight: "0.4rem" }}
/>
Pseudo Signal (optionnel)
</label>
<input
type="text"
value={signalPseudo}
onChange={(e) => setSignalPseudo(e.target.value)}
placeholder="@votre.pseudo.signal"
/>
</div>
<button
className="profile-btn profile-btn--secondary"
onClick={() => setShowSaveModal(true)}
>
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos
par défaut
</button>
</div>
{/* Section Telegram */}
{tgEnabled && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon
icon={faPaperPlane}
className="profile-card-icon profile-card-icon--telegram"
/>
Notifications Telegram
</h2>
<p className="profile-hint">
Recevez vos notifications sur Telegram, même quand
le site est fermé.
</p>
{tgLinked ? (
<div className="profile-telegram-linked">
<span className="profile-telegram-status">
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Compte Telegram lié
</span>
<button
className="profile-btn profile-btn--danger"
onClick={handleUnlinkTelegram}
>
<FontAwesomeIcon icon={faUnlink} /> Délier
Telegram
</button>
</div>
) : (
<button
className="profile-btn profile-btn--telegram"
onClick={handleLinkTelegram}
disabled={tgLoading}
>
<FontAwesomeIcon icon={faPaperPlane} />
{tgLoading
? " Génération du lien..."
: " Lier mon compte Telegram"}
</button>
)}
</div>
)}
</div>
<div className="profile-group">
<label>Téléphone (compte)</label>
<input
type="tel"
value={telephone}
onChange={(e) => setTelephone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
</div>
<button className="profile-btn" onClick={saveContact} disabled={savingContact}>
<FontAwesomeIcon icon={faSave} />
{savingContact ? ' Enregistrement...' : ' Enregistrer le compte'}
</button>
<button className="profile-btn profile-btn--secondary" onClick={() => navigate('/user/change-password')} style={{ marginTop: '0.6rem' }}>
<FontAwesomeIcon icon={faLock} /> Changer le mot de passe
</button>
</div>
{/* Section adresse par défaut */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faMapMarkerAlt} className="profile-card-icon profile-card-icon--address" />
Adresse par défaut
</h2>
<p className="profile-hint">
Sera pré-remplie dans le formulaire de commande. Vous pourrez la modifier si vous n'êtes pas à cette adresse.
</p>
<div className="profile-group">
<label>Adresse</label>
<input
type="text"
value={defaultAddress}
onChange={(e) => setDefaultAddress(e.target.value)}
placeholder="Numéro, rue, ville, code postal"
/>
</div>
</div>
{/* Section contact commande */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faPhone} className="profile-card-icon profile-card-icon--phone" />
Contact livraison
</h2>
<p className="profile-hint">
Numéro utilisé par le livreur lors de la livraison. Peut être différent du numéro de votre compte.
</p>
<div className="profile-group">
<label>Téléphone par défaut</label>
<input
type="tel"
value={defaultPhone}
onChange={(e) => setDefaultPhone(e.target.value)}
placeholder="+33 6 12 34 56 78"
/>
</div>
<div className="profile-group" style={{ marginTop: '1rem' }}>
<label>
<FontAwesomeIcon icon={faCommentDots} style={{ marginRight: '0.4rem' }} />
Pseudo Signal (optionnel)
</label>
<input
type="text"
value={signalPseudo}
onChange={(e) => setSignalPseudo(e.target.value)}
placeholder="@votre.pseudo.signal"
/>
</div>
<button className="profile-btn profile-btn--secondary" onClick={() => setShowSaveModal(true)}>
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos par défaut
</button>
</div>
{/* Section Telegram */}
{tgEnabled && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faPaperPlane} className="profile-card-icon profile-card-icon--telegram" />
Notifications Telegram
</h2>
<p className="profile-hint">
Recevez vos notifications sur Telegram, même quand le site est fermé.
</p>
{tgLinked ? (
<div className="profile-telegram-linked">
<span className="profile-telegram-status">
<FontAwesomeIcon icon={faCheckCircle} /> Compte Telegram lié
</span>
<button className="profile-btn profile-btn--danger" onClick={handleUnlinkTelegram}>
<FontAwesomeIcon icon={faUnlink} /> Délier Telegram
</button>
</div>
) : (
<button className="profile-btn profile-btn--telegram" onClick={handleLinkTelegram} disabled={tgLoading}>
<FontAwesomeIcon icon={faPaperPlane} />
{tgLoading ? ' Génération du lien...' : ' Lier mon compte Telegram'}
</button>
{showSaveModal && (
<div
className="profile-modal-overlay"
onClick={() => setShowSaveModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowSaveModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faSave} />
</div>
<h3 className="profile-modal-title">
Enregistrer les infos par défaut ?
</h3>
<p className="profile-modal-body">
Adresse, téléphone de livraison et pseudo Signal
seront sauvegardés localement et pré-remplis lors de
vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() => setShowSaveModal(false)}
>
Annuler
</button>
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveLocal}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Confirmer
</button>
</div>
</div>
</div>
)}
</div>
)}
</div>
{showSaveModal && (
<div className="profile-modal-overlay" onClick={() => setShowSaveModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowSaveModal(false)}>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faSave} />
</div>
<h3 className="profile-modal-title">Enregistrer les infos par défaut ?</h3>
<p className="profile-modal-body">
Adresse, téléphone de livraison et pseudo Signal seront sauvegardés localement et pré-remplis lors de vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--cancel" onClick={() => setShowSaveModal(false)}>
Annuler
</button>
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveLocal}>
<FontAwesomeIcon icon={faCheckCircle} /> Confirmer
</button>
</div>
</div>
</div>
)}
</>
);
</>
);
}