État et appels API
Le frontend web centralise son état dans un store Redux Toolkit. Les données du serveur passent par RTK Query, qui gère le cache, les états de chargement et l'invalidation. Trois slices classiques gardent ce qui n'est pas une réponse d'API : la session, l'état temps réel de la loterie et les modales globales.
Le store est créé par makeStore dans frontend-web/src/store/index.ts et instancié une seule fois dans Providers. Les hooks typés useAppDispatch et useAppSelector sont exportés depuis ce fichier.
flowchart LR
subgraph Store
API["blockluckyApi : cache RTK Query"]
AUTH["auth"]
LOT["lottery"]
UI["ui"]
end
P["Pages et hooks"] -->|"useXxxQuery, useXxxMutation"| API
API -->|"fetch + Bearer"| B["Backend /api"]
S["Socket.IO"] -->|"dispatch"| LOT
S -->|"invalidateTags"| API
P -->|"dispatch"| UI
H["useAuth"] --> AUTHLe client de base
frontend-web/src/store/api/baseApi.ts déclare un seul createApi (reducerPath: 'blockluckyApi'). Les cinq fichiers d'endpoints l'étendent avec injectEndpoints, si bien que tout le cache vit sous une seule clé.
| Réglage | Valeur |
|---|---|
baseUrl |
API_BASE_URL, donc NEXT_PUBLIC_API_URL ou http://localhost:2053/api |
timeout |
15 000 ms ; au-delà, l'erreur a le statut TIMEOUT_ERROR |
prepareHeaders |
Ajoute Authorization: Bearer <token> si state.auth.token existe |
| Garde 401 | baseQueryWithAuthGuard efface la session sur 401 si un jeton était présent, sauf pour /auth/2fa/* |
| Nouvelle tentative | Aucune : RTK Query ne relance pas les requêtes échouées |
Aucune requête n'envoie d'en-tête Idempotency-Key, bien que POST /lottery/buy et POST /lottery/gift acceptent le middleware d'idempotence du backend. La protection contre le double envoi repose sur le bouton désactivé pendant la requête (voir Portefeuille et loterie).
Les tags de cache
baseApi déclare douze types de tags. Une requête qui fournit un tag est rejouée automatiquement quand une mutation l'invalide, si un composant l'observe encore.
| Tag | Fourni par | Invalidé par |
|---|---|---|
User |
getProfile, getReferrals |
socialLogin, verify2FA, disable2FA, completeQuest, updatePreferences, buyTicket, giftTicket, claimReward, submitIdentity |
Lottery |
getLotteryStatus, getLeaderboard, getMyTransactions |
buyTicket, giftTicket, claimPrize, triggerDraw, refundRound, toggleCircuitBreaker ; événement draw:winner |
Wallet |
getWallet |
buyTicket, giftTicket, claimPrize ; erreurs de solde ; événement draw:winner |
Marketplace |
getCatalog, getRewardDetail, getMyCodes |
claimReward, redeemCode, setPartnerStatus, createOffer, uploadCodes |
Points |
getPointsBalance |
socialLogin, completeQuest, buyTicket, giftTicket, claimReward |
Quests |
getQuests |
completeQuest |
Notifications |
getNotifications |
aucune mutation |
Admin |
getAdminDashboard, getAnalyticsDashboard |
triggerDraw, refundRound, toggleCircuitBreaker, withdrawTreasury, setPartnerStatus ; événement draw:winner |
Partners |
getPartners, getPartnerConsumption |
createPartner, createOffer, uploadCodes, redeemCode, setPartnerStatus, createStaffAccount, updateStaffRole |
Audit |
getAuditLogs |
toggleCircuitBreaker, withdrawTreasury, setPartnerStatus, exportReport, decideIdentityVerification, createStaffAccount, updateStaffRole |
Identity |
getIdentityVerifications, getIdentityStatus |
decideIdentityVerification, submitIdentity |
Accounts |
getStaffAccounts |
createStaffAccount, updateStaffRole |
Le tag Notifications n'est jamais invalidé : la liste des notifications ne change qu'au rechargement ou par refetchNotifications.
Endpoints par fichier
Les hooks sont générés par RTK Query : useXxxQuery pour une lecture, useXxxMutation pour une écriture. Les types de réponse sont dans frontend-web/src/types. Toutes les réponses suivent l'enveloppe ApiResponse<T> du backend (success, message, data), décrite dans Réponses et erreurs.
authApi
frontend-web/src/store/api/authApi.ts
| Endpoint | Méthode et route | Type |
|---|---|---|
getOnboarding |
GET /auth/onboarding |
query |
socialLogin |
POST /auth/login ; la page de connexion envoie acceptTerms: true |
mutation |
setup2FA |
POST /auth/2fa/setup |
mutation |
verify2FA |
POST /auth/2fa/verify avec userId, code et loginTicket facultatif |
mutation |
disable2FA |
POST /auth/2fa/disable |
mutation |
logout |
POST /auth/logout (jeton passé en argument) |
mutation |
getProfile |
GET /user/profile |
query |
getIdentityStatus |
GET /user/identity : statut de la vérification d'âge, dates d'envoi et de décision, motif d'un refus ; appelé par le hook useAgeVerification, relancé au montage et au retour du focus |
query |
submitIdentity |
POST /user/identity avec front et back en data URL |
mutation |
getReferrals |
GET /user/referrals |
query |
getQuests |
GET /user/quests |
query |
completeQuest |
POST /user/quests/complete |
mutation |
updatePreferences |
PUT /user/preferences |
mutation |
getNotifications |
GET /user/notifications |
query |
exportGdprData |
GET /user/gdpr/export |
mutation |
requestGdprDeletion |
POST /user/gdpr/delete-request |
mutation |
confirmGdprDeletion |
POST /user/gdpr/confirm-delete |
mutation |
exportGdprData est déclaré en mutation alors que la route est un GET : l'export ne se lance qu'au clic et n'est jamais mis en cache.
lotteryApi
frontend-web/src/store/api/lotteryApi.ts
| Endpoint | Méthode et route | Type |
|---|---|---|
getLotteryStatus |
GET /lottery/status |
query |
getLeaderboard |
GET /lottery/leaderboard |
query |
getTransparencyCharter |
GET /lottery/transparency-charter ; plus appelé par aucune page depuis que TransparencyCharter lit content/legal.ts |
query |
getMyTransactions |
GET /lottery/my-tickets?page=1&limit=25 par défaut |
query |
buyTicket |
POST /lottery/buy avec quantity, retryAttempt |
mutation |
giftTicket |
POST /lottery/gift avec recipientEmail, personalMessage |
mutation |
getWallet |
GET /lottery/wallet |
query |
claimPrize |
POST /lottery/claim-prize avec contractRoundId |
mutation |
triggerDraw |
POST /lottery/draw |
mutation |
refundRound |
POST /lottery/refund |
mutation |
marketplaceApi
frontend-web/src/store/api/marketplaceApi.ts
| Endpoint | Méthode et route | Type |
|---|---|---|
getCatalog |
GET /marketplace/catalog |
query |
getRewardDetail |
GET /marketplace/rewards/:rewardId |
query |
getPointsBalance |
GET /marketplace/points |
query |
claimReward |
POST /marketplace/claim |
mutation |
redeemCode |
POST /marketplace/redeem |
mutation |
getMyCodes |
GET /marketplace/my-codes |
query |
adminApi
frontend-web/src/store/api/adminApi.ts
| Endpoint | Méthode et route | Type |
|---|---|---|
getAdminDashboard |
GET /admin/dashboard |
query |
getAnalyticsDashboard |
GET /analytics/dashboard |
query |
toggleCircuitBreaker |
POST /admin/circuit-breaker avec paused, reason |
mutation |
withdrawTreasury |
POST /admin/treasury/withdraw avec multisigWallet, amountEth, note |
mutation |
setPartnerStatus |
PATCH /admin/partners/:partnerId/status avec isActive |
mutation |
getAuditLogs |
GET /admin/audit-logs?limit=100 par défaut |
query |
exportReport |
GET /admin/reports/export?format=CSV ou PDF |
mutation |
getIdentityVerifications |
GET /admin/identity-verifications avec status, page, limit |
query |
getIdentityVerification |
GET /admin/identity-verifications/:id ; keepUnusedDataFor: 0, les images ne restent pas en cache après la fermeture du panneau |
query |
decideIdentityVerification |
POST /admin/identity-verifications/:id/decision avec approved et note |
mutation |
getStaffAccounts |
GET /admin/accounts avec role (ADMIN ou PARTNER), search, page, limit ; renvoie accounts et la pagination |
query |
createStaffAccount |
POST /admin/accounts avec email, role, firstName, lastName et partnerId facultatifs ; renvoie account et created |
mutation |
updateStaffRole |
PATCH /admin/accounts/:id/role avec role (USER, PARTNER ou ADMIN) |
mutation |
partnersApi
frontend-web/src/store/api/partnersApi.ts
| Endpoint | Méthode et route | Type |
|---|---|---|
getPartners |
GET /partners |
query |
createPartner |
POST /partners avec name, logoUrl |
mutation |
createOffer |
POST /partners/offers |
mutation |
uploadCodes |
POST /partners/codes/upload avec rewardId et codes ou csvContent |
mutation |
getPartnerConsumption |
GET /partners/:partnerId/consumption |
query |
La liste exhaustive des routes du backend, avec leurs schémas, est dans Référence de l'API.
Rafraîchissement
RTK Query garde une réponse en cache tant qu'un composant l'utilise, puis 60 secondes après (réglage par défaut, non modifié). Certaines pages ajoutent un pollingInterval :
| Page | Requête | Intervalle |
|---|---|---|
/tirage |
getLotteryStatus |
15 s |
/accueil |
getLotteryStatus |
30 s |
/mairie |
getAdminDashboard, getLotteryStatus |
60 s |
Les requêtes liées à la session (getPointsBalance, getNotifications) utilisent skip: !isAuthenticated. getPartnerConsumption attend qu'un établissement soit choisi (skip: !selectedId). Les autres changements arrivent par les événements Socket.IO décrits dans Temps réel.
Les slices Redux
auth
frontend-web/src/store/slices/authSlice.ts
| Champ | Rôle |
|---|---|
hydrated |
Vrai après la lecture de localStorage ; les gardes attendent ce drapeau |
isAuthenticated, token, user |
Session courante |
pendingTwoFactorUserId, pendingTwoFactorMethod, pendingTwoFactorEmail |
Vérification en cours après POST /auth/login |
pendingLoginTicket |
Ticket de connexion renvoyé par le backend quand le code de l'application est attendu ; renvoyé avec ce code à POST /auth/2fa/verify, effacé par setCredentials et logout |
Actions : hydrateSession, setCredentials, setPendingTwoFactor, updateUserPoints, updateUserProfile, logout. Le parcours complet est dans Authentification et routes protégées.
lottery
frontend-web/src/store/slices/lotterySlice.ts
| Champ | Rôle |
|---|---|
onlineWaitingUsers |
Nombre de personnes dans la salle du tour (valeur initiale 1) |
socketConnected |
État de la connexion Socket.IO |
liveChatMessages |
100 messages au plus, dédoublonnés par id ; deux messages d'accueil codés en dur au départ |
lastWinnerAnnouncement |
Dernier événement draw:winner reçu |
isRouletteSpinning |
Animation de la roue |
ui
frontend-web/src/store/slices/uiSlice.ts pilote les feuilles modales rendues par GlobalOverlays :
| Champ | Feuille | Ouvert par |
|---|---|---|
pointsPopup |
« Points crédités » | achat, cadeau, mission, événement points:awarded |
retryModal |
« Réseau encombré » avec bouton « Réessayer l'achat » | erreur 503 ou canRetry à l'achat |
queueModal |
« File d'attente active » | erreur 429 |
sentryModal |
Message d'erreur générique | autres erreurs de transaction |
notificationDrawerOpen |
Tiroir des notifications | bouton « Alertes » de l'en-tête |
osPushBanner |
Bandeau de notification simulé (4,5 s) | bouton de /profil/aide |
La feuille de file d'attente affiche toujours la position 245 : la valeur est codée en dur dans showQueueModal(245), le backend n'envoie aucune position.
Gestion des erreurs
Une mutation appelée avec .unwrap() rejette avec un objet FetchBaseQueryError. Deux formes coexistent :
| Cas | status |
data |
|---|---|---|
| Réponse HTTP du backend | nombre (400, 402, 409, 503...) |
enveloppe d'erreur : message et errors (par exemple contractError, canRetry, circuitBreaker) |
| Serveur injoignable | FETCH_ERROR |
absent |
| Délai dépassé | TIMEOUT_ERROR |
absent |
getErrorMessage(err, fallback) dans baseApi.ts renvoie, dans l'ordre : data.message du backend, un message fixe « Serveur injoignable » pour FETCH_ERROR et TIMEOUT_ERROR, sinon le texte de repli. Les pages l'utilisent pour leurs Alert.
Les erreurs des transactions de loterie suivent une table plus fine (402, 409, 429, 503, disjoncteur) dans useBlockLuckyWallet, décrite dans Portefeuille et loterie.
Les messages du backend sont rédigés en français. Ils s'affichent tels quels même quand l'interface est en anglais.