Files
swift_companion/docs/pedagogie/etape-04-vue-profil.md
Ladebeze66 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

122 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Étape 4 — Vue 2 : profil complet
> **Prérequis :** [étape 3 — recherche](etape-03-vue-recherche.md) (navigation vers vue 2 avec un login valide).
## Objectif sujet
| Exigence | Cette étape |
|----------|-------------|
| ≥ 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) |
| Projets | Statuts **`finished`** et **`failed`** |
| Erreurs | Réseau, 401, réponse API inattendue — message + réessai |
| Token | Un seul OAuth : `IntraAuthService` pour tous les `GET` |
| Retour vue 1 | `AppBar` + bouton retour système (`Navigator.pop`) |
| Layout | `LayoutBuilder` : colonne sur téléphone, 2 colonnes si largeur ≥ 720 px |
---
## Parcours utilisateur (vue 2)
1. Arrivée depuis la recherche avec un objet `user` minimal (`id`, `login`, parfois `image`).
2. **Chargement** : appels API en parallèle (spinner plein écran).
3. **Succès** : photo, carte « Informations », liste skills, liste projets.
4. **Échec** : message rouge + bouton **Réessayer**.
5. **Retour** : flèche AppBar → vue 1, nouvelle recherche possible.
---
## API 42 utilisée
Doc : [api.intra.42.fr/apidoc](https://api.intra.42.fr/apidoc)
| Donnée | Endpoint | Notes |
|--------|----------|--------|
| Fiche user | *(déjà renvoyée par la recherche étape 3)* | email, phone, location, wallet, image — pas de 2ᵉ `GET /users/{id}` pour limiter le quota |
| Fiche complète | `GET /v2/users/{login}` | inclut souvent `cursus_users` + `skills` par cursus |
| Niveau / skills | `cursus_users[].skills` | pas `/experiences` (souvent **403** avec token étudiant) |
| Projets | `GET /v2/users/{id}/projects_users?page[size]=100` | scope OAuth **`projects`** ; filtrer côté app : `finished` / `failed` |
Tous les appels :
```http
Authorization: Bearer <access_token>
Accept: application/json
```
**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
L’API renvoie un `level` flottant (ex. `8.42`) :
- **Niveau affiché** : partie entière (`8`)
- **Pourcentage** : partie décimale × 100 (`42 %`)
---
## Fichiers (étape 4)
```
lib/
├── services/
│ ├── intra_auth_service.dart # existant — headers Bearer
│ ├── intra_user_service.dart # existant — recherche
│ └── intra_profile_service.dart # NEW — agrégation profil
└── screens/
└── profile_screen.dart # StatefulWidget — chargement + UI
```
### `IntraProfileService`
- `fetchProfile(Map<String, dynamic> summaryUser)` → `IntraUserProfile`
- Requêtes **séquentielles** (pause ~450 ms) + retry sur 429 (`intra_http.dart`)
- Cache profil 2 min par `user_id` pour éviter de re-taper l’API au retour arrière
- Mêmes exceptions que l’étape 3 : `IntraNetworkException`, `IntraUnauthorizedException`, etc.
### `ProfileScreen`
- Reçoit le `user` de l’étape 3
- `initState` → `_load()`
- Pas de second OAuth
---
## Gestion des erreurs (vue 2)
| Situation | Comportement |
|-----------|----------------|
| Pas de réseau | Message + **Réessayer** |
| Token expiré (401) | « Session expirée… » — reconnecter depuis vue 1 |
| Trop de requêtes (429) | Message dédié ; retry automatique côté client + attendre avant de réessayer |
| Accès refusé (403) | Scopes `public` + `projects` sur l’app OAuth, puis **reconnexion** dans l’app |
| ID manquant | Erreur métier explicite |
| Listes vides skills / projets | Texte informatif, pas de crash |
---
## Critères « étape 4 terminée »
| # | Test |
|---|------|
| 1 | Login valide → photo + au moins 4 champs remplis ou `—` |
| 2 | Section skills avec niveau **et** pourcentage |
| 3 | Au moins un projet `finished` ou `failed` visible (si l’étudiant en a) |
| 4 | Mode avion sur vue 2 → erreur + réessai |
| 5 | Retour → vue 1 sans perdre la session OAuth |
| 6 | Largeur tablette / fenêtre large → mise en page 2 colonnes |
---
## Ensuite — étape 5
→ Affiner navigation, contraintes et polish responsive sur **les deux vues** (`etape-05-navigation-layout.md` à rédiger).