Compare commits

6 Commits

Author SHA1 Message Date
07feda8d3f notemethode 2026-10-23 09:30:32 +02:00
d8f2235e87 brancheok 2026-10-21 12:02:55 +02:00
a6ab13ee2f fix(profil): photo CDN avec Bearer OAuth et doc soutenance
Charge les avatars depuis cdn.intra.42.fr avec le même access token,
parse image.versions, et documente le flux pour la correction.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 10:38:16 +02:00
1ce33a69d1 lot4 2026-10-21 08:54:03 +02:00
4f375e6766 lot3 2026-10-21 08:50:47 +02:00
5d79d6c688 lot2 2026-10-21 08:49:37 +02:00
21 changed files with 1293 additions and 94 deletions

View File

@@ -15,6 +15,7 @@ Documentation du projet, sur le modèle de [piscine_mobile/docs](../piscine_mobi
| Document | Lien | | Document | Lien |
|----------|------| |----------|------|
| **Méthode pour un prochain projet** | [methode-demarrage-projet-ecole.md](methode-demarrage-projet-ecole.md) |
| **Reprise / état du projet** | [pedagogie/00-reprise-et-etat.md](pedagogie/00-reprise-et-etat.md) | | **Reprise / état du projet** | [pedagogie/00-reprise-et-etat.md](pedagogie/00-reprise-et-etat.md) |
| Plan des étapes | [pedagogie/README.md](pedagogie/README.md) | | Plan des étapes | [pedagogie/README.md](pedagogie/README.md) |

View File

@@ -0,0 +1,193 @@
# Méthode — démarrer un projet école
Consigne à donner à Cursor au **premier message** d’un nouveau projet 42.
Elle reprend la méthode déjà utilisée sur la piscine mobile, Swifty Companion et Tokenizer.
Copier ce fichier dans `docs/` du nouveau dépôt, ou l’ouvrir dans le chat et demander : « mets en place la structure décrite dans ce document, sans coder la fonctionnalité ».
---
## 1. Deux espaces, un seul dossier de travail
Tout vit dans **un dépôt perso** (Gitea / Git privé). Le correcteur ne voit **jamais** ce dépôt entier.
| Espace | Chemin | Qui le lit | Git école |
|--------|--------|------------|-----------|
| **Travail** | racine du dépôt : `README.md`, `docs/` | Toi + Cursor | Non |
| **Rendu** | un sous-dossier dédié, ex. `swiftyCompanion/`, `tokenizerRendu/`, `mobileModule01/` | Le correcteur, une fois copié à la racine Intra | Oui — **ce contenu devient la racine** du dépôt Vogosphere |
Règle : on **code dans le paquet de rendu**. On **explique dans `docs/`**.
Le README à la racine du dépôt de travail dit en une page : où est le sujet, où est la pédagogie, où est le paquet école, et comment pousser plus tard. Il ne remplace pas le README du rendu.
---
## 2. Ordre de démarrage (avant le code métier)
Cursor crée l’arborescence, extrait le sujet, rédige le cadrage. Il n’implémente pas l’app tant que le cadrage (section 4) n’est pas relu.
```text
<projet>/
README.md # dépôt de travail
.gitignore # .env, builds, .git imbriqué du rendu
docs/
README.md # index
methode-demarrage-projet-ecole.md # cette consigne, recopiée
sujets/
README.md
<slug>.md # traduction FR fidèle + visuels décrits
originaux/
<slug>_subject.pdf # PDF Intra, renommé (pas en.subject.pdf)
captures/ # images extraites du PDF s’il y en a
pedagogie/
README.md # feuille de route (table des étapes)
00-reprise-et-etat.md # où on en est, prochaine action
cahier-des-charges.md # exigences du sujet → critère testable
architecture.md # modules, flux, données — calé sur le sujet
ui.md # seulement si le sujet a des écrans ou maquettes
protocole-test-manuel.md # checklist device / démo, cases vides
fiche-depot.md # plus tard : comment pousser le rendu
fiche-soutenance-orale.md # plus tard : déroulé oral, reste dans docs/
<paquetRendu>/
README.md # doc école (voir section 6)
.gitignore
.env.example # seulement si le sujet a des secrets
… code …
```
Nom du paquet de rendu : celui du sujet, ou un nom explicite défendable à l’oral. Pas de deuxième copie du code à la racine du dépôt de travail.
---
## 3. Extraction du sujet (première tâche)
1. Déposer le PDF Intra dans `docs/sujets/originaux/`, renommé (`swifty_companion_subject.pdf`, pas `en.subject.pdf`).
2. Extraire le texte **et** les figures (maquettes, schémas, arborescence imposée) dans `docs/sujets/captures/` quand le PDF en contient.
3. Rédiger `docs/sujets/<slug>.md` :
- traduction française **fidèle** (le PDF reste la source si un doute apparaît) ;
- obligatoire / bonus / interdit, chacun dans sa section ;
- arborescence de rendu si le sujet en impose une ;
- messages, noms de champs, libellés : **recopiés tels quels** (souvent en anglais).
4. `docs/sujets/README.md` pointe le markdown et le PDF.
Interdit d’inventer une fonctionnalité, un écran ou une techno absents du sujet pour « améliorer » le projet. Un bonus du PDF se fait après la partie obligatoire, et se note comme bonus dans le cahier des charges.
---
## 4. Doc de dev (cadrage)
Tout ceci reste dans `docs/pedagogie/`. Aucun de ces fichiers ne part sur Vogosphere.
### `cahier-des-charges.md`
Grille de conformité. Une ligne par exigence du PDF :
| # | Exigence (citation courte ou renvoi §) | Critère de validation | Hors scope | Statut |
|---|----------------------------------------|------------------------|------------|--------|
La colonne **Hors scope** sert à figer ce qu’on ne fera pas (ex. « pas d’API météo au module 1 », « pas de mainnet »). En cas de doute, on relit le sujet ; on n’élargit pas le périmètre.
### `architecture.md`
Découpage réel du code à venir : entrée, écrans ou commandes, services (API, auth, base), où vivent les secrets, ce qui est partagé entre exercices. Schéma simple (liste ou mermaid) suffit. Chaque bloc renvoie à une exigence du cahier des charges.
### `ui.md` (si le sujet a une interface)
Un écran = une section : ce que montre la maquette, les libellés exacts, navigation, états vides / erreur / chargement **déjà décrits par le sujet**. Les captures vivent dans `docs/sujets/captures/`. On ne redessine pas.
### Feuille de route
`docs/pedagogie/README.md` est la table des étapes. Une étape = un objectif vérifiable (ex. « OAuth + `/v2/me` », « ex00 géoloc »). Statut : à faire / en cours / fait.
`00-reprise-et-etat.md` tient en une page : ce qui est fait, la prochaine étape, les commandes pour relancer. C’est le fichier à ouvrir en reprenant le projet.
Les guides détaillés (`etape-02-….md`, `module-01/ex00-guide.md`) s’écrivent **quand l’étape démarre**, pas tous d’avance.
### Rituel de lot
Pour chaque lot (cadrage, README école, fiche orale, commentaires) : Cursor décrit ce qu’il va faire → tu valides → il exécute → la case est cochée dans `00-plan` ou la feuille de route. Pas de gros chantier non relu.
---
## 5. Où coder
- Uniquement dans `<paquetRendu>/`.
- Commentaires de code en **français**. Les chaînes imposées par le sujet restent dans la langue du PDF.
- Secrets : `.env` local, listé dans `.gitignore`, jamais commité. `.env.example` versionné, sans valeur réelle. Le sujet qui punit un secret public (OAuth, clé API) est rappelé dans le README école.
- Pas de lien « pour aller plus vite » vers un autre dépôt école. La piscine peut servir de **référence perso** dans `docs/`, pas dans le paquet rendu.
---
## 6. Doc école — indépendante en fin de projet
Le README de `<paquetRendu>/` est le seul mode d’emploi du correcteur. Il contient : prérequis, installation, variables (via `.env.example`), commandes `analyze` / `test` / `run`, ce qu’il faut montrer (exercices ou écrans), et les messages d’erreur attendus s’ils sont au sujet.
**Pendant la construction**, un lien vers `docs/pedagogie/` est toléré le temps de rédiger le README (pour ne pas réécrire deux fois). **Dès que le comportement est stable**, ces liens sont retirés.
Clôture, avant branche ou push école :
```bash
# depuis le paquet de rendu — doit rester vide
grep -RIn --include='*.md' -E 'docs/pedagogie|/home/|piscine_mobile|tokenizer/docs' .
```
Le correcteur clone le dépôt école, ouvre le README à **sa** racine, et lance les commandes **depuis ce dossier**. On vérifie la même chose en local :
```bash
cd <paquetRendu>
# commandes du README, sans sortir vers la racine du dépôt de travail
```
Checklist device : `docs/pedagogie/protocole-test-manuel.md` (cases à cocher). Le README école reprend les critères utiles au correcteur, sans renvoyer vers ce protocole.
---
## 7. Git — travail et rendu
Développer sur **`main`** du dépôt perso. La doc `docs/` se commit **là**. La perdre sur une branche de rendu, c’est la perdre au checkout.
Deux formes déjà utilisées ; en choisir **une** au cadrage et l’écrire dans `fiche-depot.md`.
**A — Paquet imbriqué** (Swifty, Tokenizer). Quand le rendu est prêt : `git init` dans `<paquetRendu>/` (ou copie vers un clone Vogosphere vide). Le `.git` imbriqué est ignoré par le dépôt de travail. On ne pousse pas `docs/`.
**B — Branche snapshot** (piscine, un module = un dépôt). Quand le module est validé sur `main`, créer `rendu-…` qui ne garde que le `.gitignore` racine et le dossier du module. Si `main` change ensuite : **supprimer et recréer** la branche. Pas de merge de `main` dedans.
Dans les deux cas, la fiche orale et les guides restent sur `main` / `docs/`.
---
## 8. Préparation soutenance (après le code)
Dans l’ordre, chacun validé avant le suivant :
| Lot | Livrable | Où |
|-----|----------|----|
| README autonome | plus aucun lien vers `docs/` | `<paquetRendu>/README.md` |
| Fiche orale | déroulé, commandes, démo, questions probables, renvois au code | `docs/pedagogie/` seulement |
| Commentaires | français, sur le code du rendu | `<paquetRendu>/` |
| Dépôt école | `fiche-depot.md` exécuté ; re-test **depuis** le paquet rendu | Vogosphere |
`analyze` et `test` doivent passer sur cette copie « comme à l’école » avant le push.
---
## 9. Consigne courte à coller dans Cursor
```text
Lis docs/methode-demarrage-projet-ecole.md et applique-la à ce dépôt.
1. Range le PDF dans docs/sujets/originaux/ et produis la traduction fidèle
docs/sujets/<slug>.md (obligatoire, bonus, interdit, libellés exacts,
captures si le PDF a des visuels).
2. Crée le cadrage dans docs/pedagogie/ : reprise, cahier des charges
(une exigence = une ligne testable + hors scope), architecture,
feuille de route, ui.md seulement si le sujet a une interface,
protocole de test manuel vide.
3. Crée le paquet de rendu avec README, .gitignore et .env.example
si des secrets existent. Le README école peut linker docs/ le temps
du développement ; il sera nettoyé avant le push école.
4. Ne code pas la fonctionnalité tant que je n’ai pas validé le cadrage.
5. Ensuite, une étape de la feuille de route à la fois, code uniquement
dans le paquet de rendu, commentaires en français, aucun ajout hors sujet.
6. Réponds en français. Décris le lot, attends ma validation, puis exécute.
```

View File

@@ -0,0 +1,73 @@
# Plan de préparation — dépôt soutenance, doc et code
> **Méthode :** pour chaque lot, l’agent décrit ce qu’il compte faire → **tu valides ou ajustes** → on exécute → on coche.
**Dernière mise à jour :** lot 4 livré (commentaires `lib/` + DEBUG conservé).
---
## Décisions validées (lot 0)
| Point | Décision |
|-------|----------|
| Branche `soutenance` | Même contenu que rendu école ; `main` = travail perso ; présentation portable sur `soutenance` |
| PC école | Clone perso → copie `swiftyCompanion/` → supprimer repo perso → `git init` rendu → push Vogosphere (détail §6 fiche dépôt) |
| `.env` | **Uniquement** `swifty_companion/.env` en local ; **jamais** dans Git ; `.env.example` versionné (sujet : échec direct si secrets publics) |
| Fiche orale (lot 3) | **Déroulé de soutenance** détaillé (sujet, points critiques, réalisation, commandes expliquées) — un fichier structuré logiquement |
| Commentaires code | **Français** — sujet ne l’interdit pas |
| Bouton DEBUG | **Garder** — aide à démontrer le bonus refresh en `flutter run` ; absent en release (`kDebugMode`) — [fiche-depot-soutenance.md §8](fiche-depot-soutenance.md) |
---
## Où vit quoi
| Zone | Chemin | Git |
|------|--------|-----|
| Travail | racine `swifty_companion/` | Gitea privé (`main`) |
| Rendu / soutenance | `swiftyCompanion/` | Vogosphere + branche `soutenance` |
| Oral long | `docs/pedagogie/` | perso uniquement |
---
## Ordre des lots
| Phase | Lot | Statut |
|-------|-----|--------|
| A | 0 Cadrage | ✅ Validé |
| B | 1 Dépôt + branche | ✅ [fiche-depot-soutenance.md](fiche-depot-soutenance.md) |
| C | 2 README rendu autonome | ✅ fait |
| D | 3 Fiche soutenance orale | ✅ [fiche-soutenance-orale.md](fiche-soutenance-orale.md) |
| E | 4 Commentaires `lib/` | ✅ fait |
---
## Lot 2 — Livré
- `swiftyCompanion/README.md` réécrit (autonome, sans lien `docs/`).
- `.env.example` commenté (racine + `swifty_companion/.env.example`).
- `swifty_companion/README.md` : pointeur court vers le README racine.
## Lot 3 — Livré
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — déroulé oral, commandes commentées, démo, FAQ.
---
## Lot 4 — Livré
- Commentaires / dartdoc en **français** dans `swiftyCompanion/swifty_companion/lib/`.
- Bouton **DEBUG** conservé (`kDebugMode`) pour la démo du bonus refresh — pas présent en APK release.
**Plan préparation :** tous les lots 0–4 sont cochés ; prochaine action opérationnelle = branche `soutenance` + push Vogosphere ([fiche-depot-soutenance.md](fiche-depot-soutenance.md)).
---
## Suivi
| Lot | Fait ? |
|-----|--------|
| 0 | ✅ |
| 1 | ✅ |
| 2 | ✅ |
| 3 | ✅ |
| 4 | ✅ |

View File

@@ -2,11 +2,21 @@
Parcours pour construire le projet **étape par étape**. Parcours pour construire le projet **étape par étape**.
Pour un **nouveau** projet école, la consigne à donner à Cursor est [methode-demarrage-projet-ecole.md](../methode-demarrage-projet-ecole.md).
## Reprise rapide ## Reprise rapide
→ **[00-reprise-et-etat.md](00-reprise-et-etat.md)** — état du projet. → **[00-reprise-et-etat.md](00-reprise-et-etat.md)** — état du projet.
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — **cartographie sujet ↔ code** pour la correction (ne rien oublier). → **[00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)** — plan (valider lot par lot).
→ **[fiche-depot-soutenance.md](fiche-depot-soutenance.md)** — branche `soutenance`, `.env`, push PC école, bouton DEBUG.
→ **[fiche-soutenance-orale.md](fiche-soutenance-orale.md)** — déroulé soutenance, commandes, démo, FAQ.
→ **[protocole-test-manuel-device.md](protocole-test-manuel-device.md)** — checklist téléphone + FAQ (hidden, cache, logout, skills).
→ **[guide-soutenance-realisation.md](guide-soutenance-realisation.md)** — cartographie sujet ↔ code pour la correction.
## Prérequis ## Prérequis

View File

@@ -6,7 +6,7 @@
| Exigence | Cette étape | | Exigence | Cette étape |
|----------|-------------| |----------|-------------|
| ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet | | ≥ 4 infos + photo | login (titre / avatar), email, téléphone, niveau cursus, localisation, wallet — photo : URL API puis GET CDN avec Bearer (`_IntraProfileAvatar`) |
| Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) | | Skills | Niveau entier + **pourcentage** (partie décimale du `level` API) |
| Projets | Statuts **`finished`** et **`failed`** | | Projets | Statuts **`finished`** et **`failed`** |
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai | | Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
@@ -46,6 +46,14 @@ Accept: application/json
**Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth). **Confidentialité :** certains champs peuvent être masqués par l’API selon le profil consulté — afficher `—` si absent (sujet OAuth).
### Photo de profil
1. **URL** : extraite du JSON user (`_imageUrl` dans `profile_screen.dart`) — `image.link`, puis `image.versions`, puis `image_url`.
2. **Fichier** : `GET` sur `cdn.intra.42.fr` avec `Authorization: Bearer` (`IntraAuthService.cdnImageHeaders`) — le CDN refuse souvent les requêtes sans token.
3. **UI** : widget `_IntraProfileAvatar` (bytes en mémoire + `CircleAvatar`) ; sinon initiale du login.
Détail oral : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §5 « Photo de profil ».
### Skills — niveau et pourcentage ### Skills — niveau et pourcentage
L’API renvoie un `level` flottant (ex. `8.42`) : L’API renvoie un `level` flottant (ex. `8.42`) :

View File

@@ -0,0 +1,233 @@
# Fiche — dépôt de soutenance, branche `soutenance`, PC école
> **Lot 1** du [plan de préparation](00-plan-preparation-soutenance.md).
> **Validé :** branche `soutenance` = même contenu que Vogosphere ; travail quotidien sur `main` ; `.env` local uniquement ; `.env.example` dans le rendu Git.
---
## 1. Ce que dit le sujet (sécurité — **éliminatoire**)
Traduction du PDF v4, chapitre IV (encadré page 5) :
> Les identifiants, clés API, variables d’environnement, etc. doivent être stockés **localement** dans un fichier **`.env`** et **ignorés par git**. Des identifiants exposés **publiquement** entraînent **l’échec direct** du projet.
**À retenir pour le rendu :**
| Fichier | Dans le Git école (Vogosphere) ? |
|---------|----------------------------------|
| `.env` (avec vrais secrets) | **NON — jamais** |
| `.env.example` (modèle vide + explications) | **OUI** |
| `.gitignore` qui ignore `.env` | **OUI** |
Le correcteur crée **son** `.env` sur sa machine à partir de `.env.example` + **son** app OAuth Intra (ou la tienne en démo, **sans** committer les valeurs).
---
## 2. Où mettre le `.env` pour que l’app tourne
Flutter charge le fichier déclaré dans `pubspec.yaml` :
```yaml
assets:
- .env
```
→ chemin **obligatoire pour l’exécution** :
```text
swiftyCompanion/swifty_companion/.env
```
| Fichier | Rôle |
|---------|------|
| `swifty_companion/.env` | **Secrets réels** — machine locale (portable, école en démo) — **gitignore** |
| `swiftyCompanion/.env.example` | **Modèle** à la racine du paquet rendu — **versionné** |
| `swiftyCompanion/.env` (racine paquet) | **Inutile** pour Flutter — à **ne pas** utiliser pour éviter la confusion (tu peux le supprimer en local) |
**Procédure correcteur / toi :**
```bash
cd swiftyCompanion/swifty_companion
cp ../.env.example .env
# Éditer .env : CLIENT_ID, CLIENT_SECRET, REDIRECT_URI
flutter pub get
flutter run -d <device>
```
*(Le lot 2 enrichira `.env.example` avec commentaires détaillés par variable.)*
---
## 3. Contenu exact du dépôt école (Vogosphere)
**Racine du dépôt Intra** = contenu du dossier `swiftyCompanion/` (pas le repo perso entier).
```text
swiftyCompanion/ ← racine Git Vogosphere
├── README.md
├── .gitignore
├── .env.example
└── swifty_companion/ ← projet Flutter
├── pubspec.yaml
├── lib/
├── android/
├── test/
└── …
```
**Ne doit pas apparaître dans Vogosphere :**
- `docs/` (sujet traduit, pédagogie, fiches orale)
- tout `.env` avec secrets
- dépôt Gitea perso, liens vers guides internes
---
## 4. Deux Git, deux rôles
| Dépôt | Remote typique | Contenu |
|-------|----------------|---------|
| **Perso** | Gitea privé | `docs/` + `swiftyCompanion/` (travail) |
| **École** | Vogosphere | **uniquement** `swiftyCompanion/` à la racine |
Sur le **portable**, tu continues à coder sur **`main`** (perso). Quand le rendu est prêt, tu mets à jour la branche **`soutenance`** (voir §5) — c’est la référence « propre » pour présenter et pour préparer le push école.
---
## 5. Branche `soutenance` (portable)
### Objectif
- Même **arbre utile** que Vogosphere : tout sous `swiftyCompanion/` à jour et fonctionnel.
- Tu **peux** continuer à développer sur `main` ; `soutenance` ne bouge que quand tu **décides** de figer une version pour correction.
- En soutenance sur **ton PC** : tu ouvres le projet dans `swiftyCompanion/swifty_companion/` (pas besoin d’afficher `docs/` à l’écran).
### Méthode recommandée (monorepo perso)
```bash
cd ~/projets/42School/swifty_companion
# 1) Finir les modifs sur main (swiftyCompanion + éventuellement docs)
git checkout main
git status # rien de secret à committer
# 2) Mettre à jour soutenance (merge depuis main)
git checkout soutenance # créer une fois : git checkout -b soutenance
git merge main # ou cherry-pick des commits rendu uniquement
# 3) Vérifier que seul le paquet rendu est cohérent
cd swiftyCompanion/swifty_companion
flutter analyze && flutter test
# .env présent en local, absent du git :
git -C ../.. ls-files | rg '\.env' # doit montrer seulement .env.example
```
**Présentation :** `git checkout soutenance`, lancer `flutter run` depuis `swifty_companion/`.
**Travail après soutenance :** retour sur `main`, nouvelles features ; avant rendu final, re-merge vers `soutenance`.
### Alternative (si tu préfères un repo Git **uniquement** dans `swiftyCompanion/`)
- `git init` dans `swiftyCompanion/`, remote Vogosphere.
- Branche `main` = école, branche `dev` optionnelle.
- Le monorepo perso ne versionne plus le code Flutter (plus lourd à maintenir). **Tu as choisi le monorepo + branche `soutenance`** — la méthode ci-dessus suffit.
---
## 6. Ton scénario **PC école** (push Vogosphere)
C’est le flux que tu as décrit — formalisé pour ne rien oublier.
### Prérequis
- Branche `soutenance` à jour sur Gitea **ou** dossier `swiftyCompanion/` prêt sur clé USB / copie.
- `.env` **sur la clé ou la machine école** pour la démo (fichier local, **pas** dans le commit).
- URL du dépôt **Vogosphere** (Intra 42).
### Étapes sur le PC école
```bash
# A — Récupérer le travail (une fois)
cd ~
git clone https://ton-gitea/.../swifty_companion.git
cd swifty_companion
git checkout soutenance
# B — Préparer le dossier de rendu (contenu école pur)
# Option simple : le rendu EST le sous-dossier swiftyCompanion
cd ~/chemin/attendu/par/l/ecole # répertoire de rendu du sujet
rm -rf ./* # seulement si dossier vide / dédié — attention !
cp -a ~/swifty_companion/swiftyCompanion/. .
# C — Retirer le dépôt perso de la machine école (optionnel mais ton choix)
rm -rf ~/swifty_companion
# D — Git ÉCOLE uniquement dans le répertoire de rendu
cd ~/chemin/attendu/par/l/ecole
git init
git remote add origin <url-vogosphere-swifty_companion>
git add .
git status # VÉRIFIER : pas de .env, pas de docs/
git commit -m "Rendu Swifty Companion"
git push -u origin main
```
### Contrôles avant `git push` (checklist)
- [ ] `git status` ne liste **aucun** `.env`
- [ ] `.env.example` est présent et **sans** vraies clés
- [ ] Pas de dossier `docs/` du dépôt perso
- [ ] `flutter test` OK dans `swifty_companion/`
- [ ] README à la **racine** du clone Vogosphere (pas un niveau au-dessus)
---
## 7. Portable = démo = même binaire « logique » que l’école
| Action | Portable (`soutenance`) | Vogosphere |
|--------|-------------------------|------------|
| Code Flutter | `swiftyCompanion/swifty_companion/` | idem |
| Secrets | `.env` local | jamais poussé |
| Doc longue oral | `docs/pedagogie/` (perso) | absent |
| README correcteur | `swiftyCompanion/README.md` | idem |
---
## 8. Point « DEBUG » dans l’app (éclaircissement)
Ce n’est **pas** un fichier `.env` ni un secret.
Dans `search_screen.dart`, un bouton **« DEBUG : simuler token expiré »** n’apparaît que si `kDebugMode` est vrai (build **debug**, ex. `flutter run`). Il **n’existe pas** dans un APK **release** (`flutter build apk --release`).
| Question | Réponse |
|----------|---------|
| Le sujet l’interdit ? | **Non** — rien dans le PDF sur les boutons de debug. |
| Risque en soutenance ? | L’évaluateur peut le voir en `flutter run` ; tu peux l’expliquer pour le **bonus refresh**, ou le retirer avant rendu final si tu veux une UI plus « propre ». |
| Décision (lot 4) | **Garder** — le bonus sujet = refresh token ; le bouton ne fait qu’**invalider l’access** en debug pour tester le refresh sans attendre l’expiration. Absent en `flutter build apk --release`. |
→ Démo orale : [fiche-soutenance-orale.md](fiche-soutenance-orale.md) §7.
---
## 9. Commentaires en français dans le code
Le sujet n’impose pas la langue des commentaires. **Français = OK** (lot 4).
---
## 10. Prochaines étapes (autres lots)
| Lot | Document / action |
|-----|-------------------|
| 2 | README rendu + `.env.example` commenté (sans ton « IA ») |
| 3 | `fiche-soutenance-orale.md` — déroulé minute par minute + commandes |
| 4 | Commentaires `lib/` + décision finale bouton DEBUG |
---
## 11. Liens
- Plan global : [00-plan-preparation-soutenance.md](00-plan-preparation-soutenance.md)
- Sujet sécurité : [swifty-companion.md](../sujets/swifty-companion.md) § IV
- PDF : [originaux/swifty_companion_subject.pdf](../sujets/originaux/swifty_companion_subject.pdf)

View File

@@ -0,0 +1,342 @@
# Fiche soutenance orale — Swifty Companion
> **Usage :** répétition à voix haute, ordre de démo, **repères code** pour l’IDE.
> **Racine code à ouvrir en soutenance :** `swiftyCompanion/swifty_companion/`
> **Cartographie sujet ↔ code :** [guide-soutenance-realisation.md](guide-soutenance-realisation.md)
> **Dépôt / `.env` :** [fiche-depot-soutenance.md](fiche-depot-soutenance.md)
**Durée type peer-eval :** 5 à 15 min — viser **8 min** de démo + questions.
---
## Index rapide — où ouvrir quoi
| Sujet oral | Fichier (sous `swifty_companion/lib/` ou `test/`) |
|------------|---------------------------------------------------|
| Démarrage, `.env`, thème | `main.dart` |
| Secrets, URLs API v2 | `config/intra_config.dart` |
| Connexion OAuth, tokens, refresh | `services/intra_auth_service.dart` |
| GET + 401 → refresh + 429 | `services/intra_http.dart` |
| Recherche login | `services/intra_user_service.dart` + `screens/search_screen.dart` |
| Profil, skills, projets | `services/intra_profile_service.dart` + `screens/profile_screen.dart` |
| **Photo profil (URL + CDN Bearer)** | `profile_screen.dart` → `_imageUrl`, `_IntraProfileAvatar` · `intra_auth_service.dart` → `cdnImageHeaders` |
| Messages d’erreur | `services/intra_api_exception.dart` |
| Parse JSON token (bonus) | `services/intra_oauth_tokens.dart` + `test/intra_oauth_tokens_test.dart` |
| Layout responsive | `theme/swifty_ui.dart` |
| Redirect OAuth Android | `android/app/src/main/AndroidManifest.xml` |
| Bouton DEBUG (démo bonus) | `screens/search_screen.dart` (bloc `kDebugMode`) |
---
## Avant d’entrer en soutenance (5 min)
| Check | Action |
|-------|--------|
| Device | Téléphone chargé, USB débogage ou Wi‑Fi ADB OK |
| `.env` | `swiftyCompanion/swifty_companion/.env`, **pas** affiché à l’écran |
| OAuth Intra | Scopes **public** + **projects**, redirect = `.env` |
| Terminal | `cd swiftyCompanion/swifty_companion` |
| Git école | Pas de `.env` dans `git status` |
---
## 1. Introduction (~30 s)
**À dire :**
> « Swifty Companion : Flutter, OAuth2 Intra une fois, puis recherche par login et profil (photo, champs, skills avec %, projets validés et failed). Secrets dans `.env` local, ignoré par Git. Deux écrans, un seul access token pour toutes les requêtes API. »
| Point | Code |
|-------|------|
| Entrée + chargement `.env` | ```7:31:swiftyCompanion/swifty_companion/lib/main.dart``` |
| Vue 1 au lancement | `home: const SearchScreen()` — même fichier |
---
## 2. Sécurité et `.env` (~1 min)
**Sujet :** secrets en `.env` local, **ignorés par Git** — exposition publique = échec direct.
**Fichier exécuté :** `swiftyCompanion/swifty_companion/.env` (asset dans `pubspec.yaml`).
| Variable | Intra 42 |
|----------|----------|
| `CLIENT_ID` | UID |
| `CLIENT_SECRET` | SECRET |
| `REDIRECT_URI` | URI de redirection |
**Scopes** (Intra, pas dans `.env`) : `public` + `projects`.
| Rôle | Code |
|------|------|
| Lecture `.env` + `isConfigured` | ```9:14:swiftyCompanion/swifty_companion/lib/config/intra_config.dart``` |
| `dotenv.load` au boot | ```7:9:swiftyCompanion/swifty_companion/lib/main.dart``` |
| Message si `.env` incomplet | ```109:118:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Modèle versionné (rendu) | `swiftyCompanion/.env.example` |
---
## 3. Connexion 42 — OAuth2 pas à pas (~1–2 min)
### Ce qui se passe quand tu appuies sur « Se connecter avec 42 »
1. **`SearchScreen._connect()`** appelle **`IntraAuthService.signIn()`**
→ ```37:45:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
2. **`authorizeAndExchangeCode`** (`flutter_appauth`) : navigateur Intra → tu te connectes → redirect `com.example.swiftycompanion://callback` → l’app reçoit les tokens.
→ ```40:50:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
Scopes : `public`, `projects` — ```46:46:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
3. **Stockage en mémoire** : `_accessToken` + `_refreshToken`
→ ```57:61:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
4. **Vérification session** : `GET /v2/me` avec le même service
→ ```128:133:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
5. **Redirect Android** (schéma + host)
→ ```26:38:swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml```
**Critère sujet n°9 :** après cette étape, **plus de OAuth** pour chaque recherche — seulement `Authorization: Bearer …`
→ ```117:125:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` (`authHeaders`)
**Erreurs OAuth** (annulation, plateforme) : ```50:68:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart```
---
## 4. Tokens : access, refresh, et bouton DEBUG (à maîtriser pour le bonus)
> Section la plus importante si l’évaluateur demande le **bonus** ou le bouton **DEBUG**.
### Vocabulaire simple (à réciter)
| Jeton | Rôle | Analogie |
|-------|------|----------|
| **Access token** | Prouve à l’API que tu es autorisé ; envoyé sur chaque `GET` / `POST` API | **Badge** d’entrée — durée courte |
| **Refresh token** | Permet d’obtenir un **nouveau** access token **sans** rouvrir le navigateur OAuth | **Clé de renouvellement** du badge |
Les deux sont obtenus **une fois** au login OAuth, puis gardés dans **`IntraAuthService.instance`** (singleton).
### Flux normal (sans bouton DEBUG)
```text
[Connexion 42] → access + refresh en RAM
[Rechercher] → GET /users?filter[login]= avec Bearer access
[Profil] → GET /users/{login}, projects_users, etc. même Bearer
```
Recherche : ```82:95:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` → ```19:33:swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart```
### Quand l’access token expire (bonus sujet)
1. Un `GET` API renvoie **401 Unauthorized**.
2. **`intraApiGet`** appelle **`refreshAccessToken()`** une fois, puis **rejoue** le même `GET`.
→ ```19:24:swiftyCompanion/swifty_companion/lib/services/intra_http.dart```
3. Le refresh fait **`POST /oauth/token`** avec `grant_type=refresh_token`.
→ ```88:114:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
4. Si le refresh échoue → **`IntraUnauthorizedException`** (« Session expirée… »)
→ ```25:27:swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart```
**À dire :** « Le bonus n’est pas un second OAuth à chaque fois : c’est le refresh automatique sur 401, puis la même requête est relancée. »
### Le bouton « DEBUG : simuler token expiré » — qu’est-ce que ça fait **vraiment** ?
| Question | Réponse courte |
|----------|----------------|
| C’est demandé par le sujet ? | **Non.** C’est un **raccourci de démo** en développement. |
| C’est de la triche ? | **Non.** Il ne contourne pas l’API : il force la même situation qu’un access token périmé, pour tester le **vrai** flux refresh. |
| Visible en soutenance release ? | **Non.** Compilé seulement si `kDebugMode` (ex. `flutter run`). Absent en `flutter build apk --release`. |
**Mécanisme en 3 lignes de code :**
```143:146:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart
void debugInvalidateAccessToken() {
if (!canRefresh) return;
_accessToken = 'invalid_access_token_for_refresh_test';
}
```
- Le **refresh token n’est pas touché**.
- L’**access token** devient une valeur volontairement fausse → la prochaine API répond **401** → le chemin **bonus** (§ ci-dessus) s’exécute.
**UI du bouton :**
```189:205:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
if (kDebugMode) ...[
...
_auth.debugInvalidateAccessToken();
...
child: const Text('DEBUG : simuler token expiré'),
```
### Script oral démo DEBUG (~45 s)
1. « Je me connecte une fois à l’API 42. »
2. « Une recherche fonctionne : un seul access token, pas de nouvelle fenêtre OAuth. »
3. « En build debug j’ai un bouton qui **simule** un access token expiré — il ne supprime pas le refresh token. » *(montrer le bouton)*
4. « Je recherche à nouveau : l’API renvoie 401, mon `intra_http` déclenche le refresh, puis relance le GET — le profil s’ouvre sans repasser par le navigateur. »
5. « En production le bouton n’existe pas ; le mécanisme reste le même quand le token expire vraiment. »
**Si pas de téléphone / pas de debug :** `flutter test` sur le parse token
→ `swiftyCompanion/swifty_companion/test/intra_oauth_tokens_test.dart`
→ ```12:20:swiftyCompanion/swifty_companion/lib/services/intra_oauth_tokens.dart```
**Limite à assumer :** tokens en **mémoire** — fermer l’app = nouvelle connexion OAuth (`signOut` / kill)
→ ```64:67:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart```
---
## 5. Architecture des écrans et services
### Deux vues
| Vue | Fichier | Code clé |
|-----|---------|----------|
| Recherche + OAuth | `screens/search_screen.dart` | Classe ```14:19:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Profil | `screens/profile_screen.dart` | `fetchProfile` ```37:38:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
### Chaîne d’appels
```text
SearchScreen / ProfileScreen
→ IntraUserService / IntraProfileService
→ intraApiGet (401, 429)
→ IntraAuthService.instance
```
| Service | Fichier | Lignes utiles |
|---------|---------|---------------|
| Profil agrégé | `intra_profile_service.dart` | `fetchProfile` ~71 ; skills ~173 ; projets ~222 ; cache TTL ~65-66 |
| Erreurs typées | `intra_api_exception.dart` | fichier entier |
| Layout | `swifty_ui.dart` | `wideBreakpoint` ```13:13:swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart``` ; profil 2 col. ```103:103:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
### Photo de profil (critère sujet n°4) — à savoir expliquer
Le PDF demande **quatre détails + profile picture**. L’encadré confidentialité permet de ne pas afficher une donnée **si l’API ne la fournit pas** ; ton code **tente** d’afficher la photo dès qu’une URL existe.
#### Deux étapes (API JSON ≠ fichier image)
```text
GET /v2/users/{login} → JSON avec "image": { "link", "versions": { large, medium, … } }
↓
GET https://cdn.intra.42.fr/users/…/….jpg → bytes JPEG (hébergement CDN)
```
| Étape | Qui | Code |
|-------|-----|------|
| 1. Lire l’URL dans le JSON | `ProfileScreen._imageUrl` | ```65:83:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| 2. Télécharger l’image | `_IntraProfileAvatar._load` | ```222:236:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| 3. Header Bearer (même access token) | `IntraAuthService.cdnImageHeaders` | ```128:136:swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart``` |
| 4. Affichage | `CircleAvatar` + `MemoryImage` | ```244:251:swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart``` |
| En-tête profil | `_ProfileHeader` passe `imageUrl` | cherche `_ProfileHeader` dans `profile_screen.dart` |
**Pourquoi pas `NetworkImage(url)` seul ?** Le CDN 42 renvoie souvent **403** sans `Authorization: Bearer`. Ce n’est **pas** un nouveau OAuth : c’est le **même** access token que pour `intraApiGet`.
**Pourquoi pas `authHeaders()` sur l’image ?** Ce helper envoie `Accept: application/json`, inadapté à un JPEG — d’où `cdnImageHeaders()` (Bearer seul).
#### Script oral photo (~30 s)
> « Le sujet demande la photo de profil. L’API me donne une URL sur `cdn.intra.42.fr` dans le champ `image`. Je parse d’abord `link` puis les `versions`. Ensuite je télécharge le fichier avec le même access token que pour l’API, via `cdnImageHeaders`. Si ça échoue ou s’il n’y a pas d’URL, j’affiche l’initiale du login — c’est prévu par l’encadré confidentialité du sujet. »
#### FAQ correcteur
| Question | Réponse |
|----------|---------|
| Un token par image ? | **Non** — réutilisation du access token déjà obtenu au login. |
| Régénérer l’app OAuth pour la photo ? | **Non** — la photo dépend du JSON + CDN, pas du SECRET (sauf si OAuth cassé). |
| `public` suffit pour la photo ? | **Oui** — la photo est dans les données user publiques. |
---
## 6. Démo guidée — terminal et parcours
### Commandes (depuis `swiftyCompanion/swifty_companion/`)
| Commande | Oral |
|----------|------|
| `flutter pub get` | Dépendances |
| `flutter analyze` | *(orthographe anglaise)* analyse statique |
| `flutter test` | `test/widget_test.dart` + `intra_oauth_tokens_test.dart` |
| `flutter devices` / `flutter run -d …` | Device + lancement |
### Parcours écran
| Étape | Code à citer si demandé |
|-------|-------------------------|
| Connexion | `_connect` → `signIn` (§3) |
| Navigation profil | ```91:94:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
| Erreur profil + Réessayer | `_ErrorBody` dans `profile_screen.dart` |
| Login inconnu | `IntraUserNotFoundException` dans `intra_user_service.dart` |
| Déconnexion | `_disconnect` → `signOut` ```74:79:swiftyCompanion/swifty_companion/lib/screens/search_screen.dart``` |
---
## 7. Critères obligatoires — repères code
| # | Critère | Où montrer |
|---|---------|------------|
| 1 | 2 vues | `search_screen.dart` + `profile_screen.dart` |
| 2 | Erreurs | `intra_api_exception.dart` ; catches `search_screen` / `profile_screen` |
| 3 | Profil si login existe | Liste vide → exception, pas de `Navigator.push` — `intra_user_service.dart` ~53-55 |
| 4 | ≥4 champs + photo | `_DetailsCard` + `_imageUrl` / `_IntraProfileAvatar` — `profile_screen.dart` (§5 photo) |
| 5 | Skills niveau + % | `_parseSkillsFromCursus` ```173:207:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
| 6 | Projets + failed | `_parseProjects` ```222:246:swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart``` |
| 7 | Retour vue 1 | `showBack: true` sur AppBar profil ; `Navigator.pop` implicite |
| 8 | Layout flexible | `ResponsiveScrollBody`, `LayoutBuilder` wide 720px |
| 9 | Un token | `IntraAuthService.instance` ; pas de `signIn` dans `_search` |
**Champs `—` :** confidentialité OAuth — conforme sujet.
---
## 8. Erreurs — quoi montrer
| Cas | Code / message |
|-----|----------------|
| Login vide | `intra_user_service.dart` ~20-22 |
| Non connecté | `IntraNotAuthenticatedException` |
| Réseau | `IntraNetworkException` dans user/profile services |
| 403 scopes | `IntraForbiddenException` |
| 429 | Retry ```27:32:swiftyCompanion/swifty_companion/lib/services/intra_http.dart``` |
| Session morte | `IntraUnauthorizedException` après échec refresh |
---
## 9. Questions fréquentes
| Question | Réponse + code |
|----------|----------------|
| Pourquoi pas `/experiences` ? | Skills depuis `cursus_users` — `_parseSkillsFromCursus` |
| Pourquoi scope `projects` ? | `projects_users` ; sinon 403 |
| Pourquoi le bouton DEBUG ? | Démo du bonus refresh en debug — §4 |
| Régénérer SECRET pour la photo ? | **Non** — voir §5 photo (CDN + Bearer) |
| Comment charge-t-on la photo ? | `_imageUrl` puis `_IntraProfileAvatar` + `cdnImageHeaders` |
| Tests bonus ? | `test/intra_oauth_tokens_test.dart` |
---
## 10. Script condensé (1 page)
1. Intro — Flutter, OAuth une fois, 2 vues, `.env` hors Git (`main.dart`, `intra_config.dart`).
2. `flutter test`.
3. Connexion — `signIn` + `AndroidManifest` redirect.
4. Recherche → profil — `intra_user_service`, `intra_profile_service`.
5. Retour → 2ᵉ recherche (même token).
6. Erreurs — login faux, réseau.
7. Bonus — expliquer access/refresh ; option DEBUG → `debugInvalidateAccessToken` + `intra_http` 401.
8. Rappel — 9 critères, scopes, `.env.example` au rendu.
---
## 11. Liens doc perso
| Document | Usage |
|----------|--------|
| [guide-soutenance-realisation.md](guide-soutenance-realisation.md) | Tableau sujet ↔ fichiers détaillé |
| [etape-07-bonus-token.md](etape-07-bonus-token.md) | Refresh approfondi |
| [fiche-depot-soutenance.md](fiche-depot-soutenance.md) | DEBUG §8, Vogosphere |
| [swiftyCompanion/README.md](../../swiftyCompanion/README.md) | README correcteurs |
---
*Dernière mise à jour : photo CDN (Bearer), commentaires code, guide + README rendu.*

View File

@@ -77,7 +77,7 @@
| Donnée affichée | Source API | Fichier UI | | Donnée affichée | Source API | Fichier UI |
|-----------------|------------|------------| |-----------------|------------|------------|
| **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` | | **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` |
| **Photo** | `image.link` ou `image_url` | `CircleAvatar` + `NetworkImage` — `_ProfileHeader` | | **Photo** | `image.link`, `image.versions`, `image_url` | `_imageUrl` + `_IntraProfileAvatar` (`http.get` + Bearer via `cdnImageHeaders`) — `profile_screen.dart` |
| **Email** | `user['email']` | `_DetailsCard` | | **Email** | `user['email']` | `_DetailsCard` |
| **Téléphone** | `user['phone']` | `_DetailsCard` | | **Téléphone** | `user['phone']` | `_DetailsCard` |
| **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` | | **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` |

View File

@@ -0,0 +1,200 @@
# Protocole de test manuel sur téléphone — Swifty Companion
> **Objectif :** valider la partie obligatoire (et le bonus) **sur device** avant de figer la branche `soutenance` et le rendu Vogosphere.
> **Code :** `swiftyCompanion/swifty_companion/` · **Branche travail :** `main` · **Branche portable / rendu :** `soutenance`.
**Prérequis PC :**
```bash
cd swiftyCompanion/swifty_companion
flutter pub get && flutter analyze && flutter test
flutter devices
flutter run -d <id_téléphone>
```
`.env` dans `swifty_companion/.env` · OAuth Intra : scopes **public** + **projects** · redirect = `com.example.swiftycompanion://callback`.
---
## Réponses aux points soulevés (à lire avant de cocher)
### A — Déconnexion (icône logout) vs annulation OAuth
| Action | Comportement actuel | Faut-il un message « connexion annulée » ? |
|--------|---------------------|---------------------------------------------|
| **Annuler** dans le navigateur OAuth (pendant « Se connecter avec 42 ») | Message **« Connexion annulée »** | Déjà géré — `search_screen.dart` (`FlutterAppAuthUserCancelledException`). |
| **Déconnexion** (icône logout en haut à droite quand connecté) | Retour à l’écran **« Connecte-toi »** sans message | **Non obligatoire** au sujet. C’est une **déconnexion volontaire**, pas une annulation OAuth. Optionnel : SnackBar « Déconnecté » — pas requis pour valider. |
**Test A3** du protocole = annuler **pendant** le flux navigateur, pas appuyer sur logout.
### B3 — Champs profil : `hidden`, vide, ≥ 4 détails
Le sujet demande **au moins quatre détails** parmi login, email, mobile, level, location, wallet, evaluations, etc. **+ photo**.
| Ce que tu vois | Conforme ? |
|----------------|------------|
| Email affiché | Oui — compte comme 1 détail |
| Téléphone = **`hidden`** | Oui — c’est la **valeur renvoyée par l’API** (confidentialité), pas un bug |
| Niveau cursus affiché | Oui |
| Localisation vide → **`—`** | Oui — souvent pas de poste connecté / pas sur PC école ; encadré sujet confidentialité |
| Wallet (points) | Oui |
| **Login** + **photo** (en-tête) | Comptent aussi pour le critère |
Tu as **plus de 4 lignes utiles** même si certaines sont `hidden` ou `—`. **Pas besoin d’ajouter** d’autres champs (evaluation_points, etc.) sauf choix perso.
Autres champs possibles dans l’API (non requis) : `displayname`, `correction_point`, `pool_month`, … — le sujet ne impose pas de tout afficher.
### B4 — « Niveau entier » pour les skills
L’API envoie un niveau **décimal** (ex. `8.42`).
| Affichage app | Signification |
|---------------|----------------|
| **Niveau 8** | Partie **entière** (`floor`) — c’est le « niveau entier » du sujet |
| **(42 %)** | Partie décimale × 100 — le **pourcentage** du sujet |
Exemple ligne UI : `Algorithms — niveau 8 (42 %)`.
Code : `IntraProfileService._skillFromLevel` · affichage `_SkillsCard` dans `profile_screen.dart`.
Plusieurs skills affichés = **mieux** ; le sujet exige le **format** niveau + % sur les skills montrés.
### C5 — Profil ouvert + réseau coupé (cache)
| Comportement | Explication |
|--------------|-------------|
| Profil **déjà chargé** reste visible sans réseau | Cache mémoire ~2 min (`IntraProfileService`) + données déjà en RAM — **normal** |
| Erreur réseau au **retour** puis nouvelle recherche | Couvre le critère « erreur réseau » |
| Erreur + **Réessayer** si chargement profil échoue | Déjà implémenté (`_ErrorBody`) |
Le sujet **n’exige pas** un rafraîchissement automatique du profil quand le réseau tombe. Test **C5 = optionnel** (amélioration possible plus tard, pas bloquant soutenance).
### F — Après déconnexion
Comportement attendu et **correct** :
- Plus de session → écran **connexion OAuth** uniquement ;
- **Pas** de recherche tant que tu ne t’es pas reconnecté (`IntraNotAuthenticatedException` si tu forçais une recherche — l’UI masque le formulaire tant que déconnecté).
---
## Grille de test — à cocher sur le téléphone
Légende : **O** = OK · **N** = échec · **—** = non testé / optionnel
### Préparation
| ID | Vérification | O / N / — |
|----|--------------|-----------|
| P1 | `flutter test` vert sur la branche testée | |
| P2 | `.env` présent, app démarre sans erreur config | |
| P3 | App installée via `flutter run` (debug) | |
### A — OAuth
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| A1 | **Se connecter avec 42** (succès) | Session affichée, formulaire recherche visible | |
| A2 | **Déconnexion** (icône logout) | Écran connexion, **sans** message obligatoire | |
| A3 | Reconnecter, lancer OAuth puis **annuler** dans le navigateur | Message **« Connexion annulée »** (pas confondre avec A2) | |
| A4 | Reconnexion réussie | Session OK | |
### B — Parcours nominal
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| B1 | Recherche login valide | Écran profil | |
| B2 | Photo | Image visible (ou initiale si API sans URL — expliquer à l’oral) | |
| B3 | Carte informations | ≥ 4 lignes (email, tel même `hidden`, niveau, wallet, etc.) | |
| B4 | Skills | Au moins une ligne `niveau X (Y %)` | |
| B5 | Projets | Au moins un validé et idéalement un **failed** | |
| B6 | Retour arrière | Recherche, **toujours connecté** | |
| B7 | 2ᵉ recherche (autre login) | Profil sans nouveau OAuth | |
### C — Erreurs
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| C1 | Recherche login vide | Message « Saisis un login » | |
| C2 | `zzzzinvalid` | Message, pas de profil vide | |
| C3 | Mode avion → **rechercher** | Message réseau | |
| C4 | Réseau OK → recherche à nouveau | Profil OK | |
| C5 | *(Optionnel)* Profil ouvert → couper réseau | Données en cache OK ; pas exigé par le sujet | — |
### D — Layout
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| D1 | Rotation paysage (profil) | Lisible / 2 colonnes si large | |
| D2 | Clavier sur écran recherche | Scroll OK | |
### E — Bonus (optionnel)
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| E1 | DEBUG simuler token expiré | Message debug | |
| E2 | Rechercher ensuite | Profil sans navigateur OAuth | |
### F — Session (complément)
| ID | Action | Résultat attendu | O / N / — |
|----|--------|------------------|-----------|
| F1 | Déconnecté : pas de champ recherche utilisable | Uniquement bouton connexion | |
---
## Validation sujet (après grille)
| # | Critère | Validé si… | O / N |
|---|---------|------------|-------|
| 1 | 2 vues | B1, B6 | |
| 2 | Erreurs | C1–C4 (+ A3) | |
| 3 | Profil si login existe | C2 vs B1 | |
| 4 | 4 détails + photo | B2, B3 | |
| 5 | Skills niveau + % | B4 | |
| 6 | Projets + failed | B5 | |
| 7 | Retour vue 1 | B6 | |
| 8 | Layout | D1 ou D2 | |
| 9 | Un token | B7 | |
| Bonus | Refresh | E1–E2 | |
**Décision :** si tous les **O / N** obligatoires sont **O** → passer à la **mise à jour branche `soutenance`** (voir ci-dessous).
---
## Après validation manuelle — branche `soutenance`
1. `git checkout main` → `git push origin main` (si pas déjà fait).
2. Aligner `main` / `soutenance` si besoin (ex. un seul `.env.example` sur `main`).
3. `git checkout soutenance` → vérifier commit photo (`eaacef0` ou plus récent).
4. **Re-test sur `soutenance` :**
```bash
cd swiftyCompanion/swifty_companion
flutter pub get && flutter analyze && flutter test
flutter run -d <device>
```
5. Refaire **minimum** : A1, B1–B7, C2, B2 (photo).
6. `git push origin soutenance`.
---
## Améliorations **non** bloquantes (ne pas faire avant validation sauf si tu veux)
| Idée | Priorité |
|------|----------|
| SnackBar « Déconnecté » au logout | Cosmétique |
| Pull-to-refresh profil quand réseau revient | Hors sujet |
| Afficher `evaluation_points` | Optionnel sujet |
| Invalider cache profil à la perte réseau | Optionnel |
---
## Liens
- [fiche-soutenance-orale.md](fiche-soutenance-orale.md) — oral + repères code
- [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) — rendu Git
- [guide-soutenance-realisation.md](guide-soutenance-realisation.md) — sujet ↔ fichiers
*Dernière mise à jour : retours test device (logout, hidden, cache profil, skills).*

View File

@@ -1,7 +1,37 @@
# Renommer / copier en .env — ne jamais committer .env # =============================================================================
# Application OAuth : https://profile.intra.42.fr/oauth/applications # Swifty Companion — modèle de configuration (PAS de secrets réels ici)
# =============================================================================
#
# 1) Copier ce fichier vers le package Flutter :
# cp .env.example swifty_companion/.env
# (depuis la racine de ce dépôt, dossier swiftyCompanion/)
#
# 2) Remplir les valeurs depuis ton application OAuth sur l’Intra 42 :
# https://profile.intra.42.fr/oauth/applications
#
# 3) Ne JAMAIS committer le fichier .env (le sujet prévoit l’échec direct si
# des identifiants sont exposés publiquement). Ce .env.example peut être
# versionné : il ne contient que des noms de variables et des exemples.
#
# =============================================================================
# CLIENT_ID — sur l’Intra, champ « UID » de l’application OAuth.
# C’est l’identifiant public de ton app (pas ton login 42).
# Exemple de forme : u-s4t2ud-xxxxxxxx...
CLIENT_ID= CLIENT_ID=
# CLIENT_SECRET — sur l’Intra, champ « SECRET » de l’application OAuth.
# Sert à échanger le code OAuth contre un access token. À garder strictement
# privé : uniquement dans .env local, jamais sur Git ni dans le chat.
# Exemple de forme : s-s4t2ud-xxxxxxxx...
CLIENT_SECRET= CLIENT_SECRET=
# Identique au champ « URI de redirection » sur l’Intra (une seule ligne, sans guillemets)
# REDIRECT_URI — URI de redirection enregistrée sur l’Intra (caractère pour
# caractère identique dans le formulaire et ici).
# Pour un téléphone Android avec ce projet : schéma custom + host callback
# (voir aussi android/app/src/main/AndroidManifest.xml).
REDIRECT_URI=com.example.swiftycompanion://callback REDIRECT_URI=com.example.swiftycompanion://callback
# Scopes à cocher sur l’Intra lors de la création de l’app (pas dans .env) :
# - public (données publiques utilisateur)
# - projects (liste projects_users sur le profil — sans cela, erreur 403)

View File

@@ -1,87 +1,102 @@
# Swifty Companion # Swifty Companion
Application mobile — profil étudiant 42 via l’**API Intra** (OAuth2). Application mobile Flutter : recherche d’un login 42 et affichage du profil (API Intra, OAuth2).
> **Dépôt de rendu école** : ce dossier est la **racine** du dépôt Git Vogosphere. Pas de `docs/` ni de sujet traduit ici (dépôt perso séparé). ## Prérequis
**État :** partie obligatoire + **bonus refresh token** (401 → refresh → retry API). - Flutter SDK (voir [flutter.dev](https://flutter.dev))
- Un téléphone ou émulateur Android configuré (`flutter doctor`)
- Une application OAuth créée sur l’Intra : [Applications API](https://profile.intra.42.fr/oauth/applications)
## Lancer le projet ## Installation
À la racine de **ce** dépôt Git (dossier qui contient `swifty_companion/`) :
```bash ```bash
cd swifty_companion cd swifty_companion
# Créer .env ici (voir .env.example à la racine du dépôt école) cp ../.env.example .env
flutter pub get
flutter run -d <device>
``` ```
Le fichier **`.env`** doit être dans **`swifty_companion/.env`** (déclaré comme asset dans `pubspec.yaml`). Édite `swifty_companion/.env` : renseigne `CLIENT_ID`, `CLIENT_SECRET` et `REDIRECT_URI` (voir commentaires dans `.env.example` à la racine).
Vérifications : Puis :
```bash ```bash
flutter doctor flutter pub get
flutter devices flutter run -d <identifiant_appareil>
```
Lister les appareils : `flutter devices`.
## Configuration OAuth (résumé)
| Variable `.env` | Sur l’Intra 42 |
|-----------------|----------------|
| `CLIENT_ID` | **UID** de l’application |
| `CLIENT_SECRET` | **SECRET** de l’application |
| `REDIRECT_URI` | **URI de redirection** (ex. `com.example.swiftycompanion://callback`) |
Scopes à activer sur l’application : **public** et **projects**.
La redirect URI doit être la même sur l’Intra, dans `.env`, et dans la config Android du projet.
**Important :** le fichier `.env` reste sur ta machine uniquement. Ce dépôt contient `.env.example` sans vraies clés. Ne pousse jamais `.env` sur le Git de rendu.
Documentation API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
## Utilisation
1. Ouvrir l’app → **Se connecter avec 42** (une fois par session).
2. Saisir un **login** → **Rechercher**.
3. Consulter le **profil** (photo, informations, skills, projets validés ou en échec).
4. **Retour** → nouvelle recherche avec le même token (pas de nouvelle connexion OAuth à chaque fois).
Certaines données peuvent s’afficher comme « — » : l’API peut masquer des champs selon la confidentialité.
**Photo :** l’URL est fournie par l’API (`image` sur la fiche user) ; le fichier JPEG est chargé depuis `cdn.intra.42.fr` avec le **même** access token OAuth (pas un second login).
## Vérifications avant soutenance
```bash
cd swifty_companion
flutter analyze flutter analyze
flutter test flutter test
``` ```
## Configuration (`.env`) ## Critères du sujet (v4) couverts par l’app
1. App OAuth : [profile.intra.42.fr/oauth/applications](https://profile.intra.42.fr/oauth/applications)
2. Scopes : **`public`** + **`projects`** (projets sur le profil)
3. Redirect URI (ex. téléphone) : `com.example.swiftycompanion://callback` — identique sur l’Intra et dans `.env`
4. Ne **jamais** committer `.env`
API : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
## Parcours app
1. **Se connecter avec 42** (token réutilisé pour toutes les requêtes)
2. **Rechercher** un login
3. **Profil** : photo, champs, skills (niveau + %), projets validés et failed
4. **Retour** → nouvelle recherche
## Partie obligatoire (rappel sujet v4)
| # | Exigence | | # | Exigence |
|---|----------| |---|----------|
| 1 | Au moins **2 vues** | | 1 | Deux écrans : recherche et profil |
| 2 | **Toutes** les erreurs gérées (login inconnu, réseau, …) | | 2 | Erreurs gérées (login inconnu, réseau, session, etc.) |
| 3 | Vue 2 : infos du login s’il **existe** | | 3 | Profil affiché si le login existe |
| 4 | **≥ 4 détails** + **photo de profil** | | 4 | Au moins quatre informations + photo |
| 5 | **Skills** : niveau + pourcentage | | 5 | Skills avec niveau et pourcentage |
| 6 | **Projets** réalisés, **y compris les failed** | | 6 | Projets terminés, y compris en échec |
| 7 | Retour possible vers la **vue 1** | | 7 | Retour vers l’écran de recherche |
| 8 | Layout **adaptatif** | | 8 | Mise en page adaptée (scroll, grands écrans) |
| 9 | **OAuth2** — **un token** réutilisé | | 9 | Un seul flux OAuth, token réutilisé pour les appels API |
Certaines données peuvent être masquées par l’API (confidentialité).
## Bonus ## Bonus
- Refresh token : `IntraAuthService.refreshAccessToken()` + retry dans `intraApiGet` sur 401. Rafraîchissement du token d’accès lorsque l’API renvoie 401, puis nouvelle tentative de la requête (sans refaire l’écran de connexion 42).
## Soutenance — checklist peer-eval ## Déroulé de démonstration (peer-eval)
| # | Test | Démo | 1. `flutter test`
|---|------|------| 2. `flutter run` → connexion 42 → login valide → profil complet
| 1 | `flutter test` | Tests OK | 3. Login invalide → message d’erreur
| 2 | `flutter run -d <device>` | Vue 1 → login valide → vue 2 | 4. Mode avion (ou réseau coupé) → message réseau
| 3 | Profil | Photo + ≥ 4 champs + skills + projets (dont failed) | 5. Retour à la recherche → nouvelle recherche
| 4 | Login invalide | Message clair, pas de crash | 6. Rotation ou petit écran → interface lisible
| 5 | Réseau coupé | Erreur réseau gérée | 7. Bonus : expliquer ou montrer le refresh token si demandé
| 6 | Navigation | Retour vue 1, nouvelle recherche |
| 7 | Petit écran / rotation | UI lisible |
| 8 | Bonus | Refresh token si implémenté |
**Ordre suggéré :** connexion → profil → login faux → hors ligne → retour → `flutter test`. ## Structure du dépôt
## Fichiers de ce dépôt ```text
.
| Fichier | Rôle | ├── README.md # ce fichier
|---------|------| ├── .gitignore
| `README.md` | Ce document | ├── .env.example # modèle (sans secrets)
| `.gitignore` | Exclut `.env`, build, etc. | └── swifty_companion/ # code Flutter (lib/, android/, test/, …)
| `.env.example` | Modèle sans secrets | ```
| `swifty_companion/` | Projet Flutter (`lib/`, `android/`, …) |

View File

@@ -0,0 +1,9 @@
# Copier en .env dans CE dossier (swifty_companion/) :
# cp .env.example .env
# Puis éditer .env avec ton UID, SECRET et REDIRECT_URI Intra.
#
# Modèle détaillé (mêmes variables) : ../.env.example à la racine du dépôt.
CLIENT_ID=
CLIENT_SECRET=
REDIRECT_URI=com.example.swiftycompanion://callback

View File

@@ -1,17 +1,14 @@
# swifty_companion # Package Flutter — Swifty Companion
A new Flutter project. Le projet s’exécute depuis ce dossier.
## Getting Started ## Démarrage rapide
This project is a starting point for a Flutter application. ```bash
cp .env.example .env # ou : cp ../.env.example .env
# Éditer .env (CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
flutter pub get
flutter run -d <device>
```
A few resources to get you started if this is your first Flutter project: Instructions complètes (OAuth, soutenance, structure du dépôt) : **README.md à la racine du dépôt** (dossier parent).
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -1,3 +1,4 @@
// Point d'entrée : charge les secrets depuis `.env` (asset, non versionné) puis vue recherche.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';

View File

@@ -1,10 +1,14 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import '../services/intra_api_exception.dart'; import '../services/intra_api_exception.dart';
import '../services/intra_auth_service.dart';
import '../services/intra_profile_service.dart'; import '../services/intra_profile_service.dart';
import '../theme/swifty_ui.dart'; import '../theme/swifty_ui.dart';
/// Vue 2 — profil complet (étape 4). /// Vue 2 — photo, ≥ 4 champs, skills (niveau + %), projets validés et failed.
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key, required this.user}); const ProfileScreen({super.key, required this.user});
@@ -56,11 +60,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
} }
/// Extrait l’URL photo depuis la fiche `GET /v2/users/{login}` (critère sujet : photo de profil).
///
/// Ordre : `image.link` → `image.versions` (large…micro) → champ legacy `image_url`.
String? _imageUrl(Map<String, dynamic> user) { String? _imageUrl(Map<String, dynamic> user) {
final image = user['image']; final image = user['image'];
if (image is Map<String, dynamic>) { if (image is Map<String, dynamic>) {
final link = image['link']; final link = image['link'];
if (link is String && link.isNotEmpty) return link; if (link is String && link.isNotEmpty) return link;
final versions = image['versions'];
if (versions is Map<String, dynamic>) {
for (final key in ['large', 'medium', 'small', 'micro']) {
final v = versions[key];
if (v is String && v.isNotEmpty) return v;
}
}
} }
final url = user['image_url']; final url = user['image_url'];
if (url is String && url.isNotEmpty) return url; if (url is String && url.isNotEmpty) return url;
@@ -172,6 +187,71 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
} }
/// Avatar profil : charge l’image via HTTP avec le Bearer OAuth (`IntraAuthService.cdnImageHeaders`).
///
/// Un simple `NetworkImage(url)` sans token échoue souvent sur le CDN 42. Si le chargement
/// échoue ou l’URL est absente, affiche l’initiale du login (conforme encadré confidentialité sujet).
class _IntraProfileAvatar extends StatefulWidget {
const _IntraProfileAvatar({required this.login, required this.imageUrl});
final String login;
final String? imageUrl;
@override
State<_IntraProfileAvatar> createState() => _IntraProfileAvatarState();
}
class _IntraProfileAvatarState extends State<_IntraProfileAvatar> {
Uint8List? _bytes;
@override
void initState() {
super.initState();
_load();
}
@override
void didUpdateWidget(covariant _IntraProfileAvatar oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.imageUrl != widget.imageUrl) {
_bytes = null;
_load();
}
}
/// GET binaire sur l’URL CDN ; même session que les appels `api.intra.42.fr`.
Future<void> _load() async {
final url = widget.imageUrl;
if (url == null || url.isEmpty) return;
final headers = IntraAuthService.instance.cdnImageHeaders();
try {
final response = await http.get(Uri.parse(url), headers: headers);
if (response.statusCode == 200 && mounted) {
setState(() => _bytes = response.bodyBytes);
}
} catch (_) {
// Initiale affichée en repli.
}
}
Widget _initial() {
final letter =
widget.login.isNotEmpty ? widget.login[0].toUpperCase() : '?';
return Text(letter, style: const TextStyle(fontSize: 32));
}
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: 56,
backgroundImage:
_bytes != null ? MemoryImage(_bytes!) : null,
child: _bytes == null ? _initial() : null,
);
}
}
class _ErrorBody extends StatelessWidget { class _ErrorBody extends StatelessWidget {
const _ErrorBody({required this.message, required this.onRetry}); const _ErrorBody({required this.message, required this.onRetry});
@@ -206,17 +286,7 @@ class _ProfileHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
CircleAvatar( _IntraProfileAvatar(login: login, imageUrl: imageUrl),
radius: 56,
backgroundImage:
imageUrl != null ? NetworkImage(imageUrl!) : null,
child: imageUrl == null
? Text(
login.isNotEmpty ? login[0].toUpperCase() : '?',
style: const TextStyle(fontSize: 32),
)
: null,
),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
login, login,

View File

@@ -186,6 +186,7 @@ class _SearchScreenState extends State<SearchScreen> {
loading: _searching, loading: _searching,
onPressed: _search, onPressed: _search,
), ),
// Bonus refresh : invalide l'access token en mémoire (build debug uniquement).
if (kDebugMode) ...[ if (kDebugMode) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
TextButton( TextButton(

View File

@@ -125,6 +125,16 @@ class IntraAuthService {
}; };
} }
/// En-têtes pour télécharger la photo sur `cdn.intra.42.fr`.
///
/// L’URL vient du JSON user (`image.link` / `versions`). Le CDN refuse souvent
/// les requêtes sans Bearer (403). On réutilise le **même** access token que pour
/// l’API — pas un second OAuth. On n’envoie pas `Accept: application/json` ici.
Map<String, String> cdnImageHeaders() {
if (!isAuthenticated) return const {};
return {'Authorization': 'Bearer $_accessToken'};
}
Future<Map<String, dynamic>> fetchMe() async { Future<Map<String, dynamic>> fetchMe() async {
final response = await intraApiGet( final response = await intraApiGet(
Uri.parse('${IntraConfig.apiBaseUrl}/me'), Uri.parse('${IntraConfig.apiBaseUrl}/me'),
@@ -139,8 +149,7 @@ class IntraAuthService {
_refreshToken = refresh; _refreshToken = refresh;
} }
/// Simule un access token expiré (garde le refresh) — tests bonus en debug. /// Simule un access token expiré (garde le refresh) — démo bonus, appelé depuis l’UI en `kDebugMode`.
@visibleForTesting
void debugInvalidateAccessToken() { void debugInvalidateAccessToken() {
if (!canRefresh) return; if (!canRefresh) return;
_accessToken = 'invalid_access_token_for_refresh_test'; _accessToken = 'invalid_access_token_for_refresh_test';

View File

@@ -50,6 +50,7 @@ Duration _retryDelay(http.Response response, int attempt) {
return Duration(milliseconds: ms.clamp(800, 8000)); return Duration(milliseconds: ms.clamp(800, 8000));
} }
/// Lance une [IntraApiException] si le code HTTP n'est pas 200.
void ensureIntraOk(http.Response response) { void ensureIntraOk(http.Response response) {
if (response.statusCode == 401) { if (response.statusCode == 401) {
throw const IntraUnauthorizedException(); throw const IntraUnauthorizedException();
@@ -67,6 +68,7 @@ void ensureIntraOk(http.Response response) {
} }
} }
/// Décode le corps JSON après contrôle du statut HTTP.
dynamic decodeIntraBody(http.Response response) { dynamic decodeIntraBody(http.Response response) {
ensureIntraOk(response); ensureIntraOk(response);
return jsonDecode(response.body); return jsonDecode(response.body);

View File

@@ -55,10 +55,12 @@ class _ProfileCacheEntry {
final DateTime fetchedAt; final DateTime fetchedAt;
} }
/// Agrège fiche user, skills (cursus, pas `/experiences`) et `projects_users` (scope `projects`).
class IntraProfileService { class IntraProfileService {
IntraProfileService._(); IntraProfileService._();
static final IntraProfileService instance = IntraProfileService._(); static final IntraProfileService instance = IntraProfileService._();
/// Cache court + délai entre GET pour limiter les 429 sur l'API 42.
static const Duration _cacheTtl = Duration(minutes: 2); static const Duration _cacheTtl = Duration(minutes: 2);
static const Duration _betweenRequests = Duration(milliseconds: 450); static const Duration _betweenRequests = Duration(milliseconds: 450);

View File

@@ -8,6 +8,7 @@ import 'intra_api_exception.dart';
import 'intra_auth_service.dart'; import 'intra_auth_service.dart';
import 'intra_http.dart'; import 'intra_http.dart';
/// Recherche d'un login via `GET /v2/users?filter[login]=` (même token OAuth que le profil).
class IntraUserService { class IntraUserService {
IntraUserService._(); IntraUserService._();
static final IntraUserService instance = IntraUserService._(); static final IntraUserService instance = IntraUserService._();

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Constantes UI partagées (étape 5 — layout). /// Constantes UI partagées (layout responsive, thème 42).
abstract final class SwiftyUi { abstract final class SwiftyUi {
static const Color brand = Color(0xFF00BABC); static const Color brand = Color(0xFF00BABC);
@@ -66,6 +66,7 @@ class ResponsiveScrollBody extends StatelessWidget {
} }
} }
/// Message d'erreur centré (recherche ou profil).
class ErrorMessage extends StatelessWidget { class ErrorMessage extends StatelessWidget {
const ErrorMessage({super.key, required this.message}); const ErrorMessage({super.key, required this.message});
@@ -81,6 +82,7 @@ class ErrorMessage extends StatelessWidget {
} }
} }
/// Bouton principal avec indicateur de chargement.
class LoadingFilledButton extends StatelessWidget { class LoadingFilledButton extends StatelessWidget {
const LoadingFilledButton({ const LoadingFilledButton({
super.key, super.key,