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>
122 lines
4.8 KiB
Markdown
122 lines
4.8 KiB
Markdown
# É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).
|