# Guide de correction / soutenance — ce qui a été réalisé > **Objectif :** suivre le [sujet Swifty Companion v4](../sujets/swifty-companion.md) point par point, avec **fichiers**, **comportement** et **démo** à ne pas oublier en peer-eval. > **Code évalué :** dépôt école = dossier [`swiftyCompanion/`](../../swiftyCompanion/) (pas `docs/`). --- ## 1. Vue d’ensemble | Élément | Détail | |---------|--------| | Framework | **Flutter** (Dart), Material 3 | | API | **42 Intra v2** — `https://api.intra.42.fr/v2` | | Auth | **OAuth2** — `flutter_appauth` 12.x | | Secrets | **`.env`** dans `swiftyCompanion/swifty_companion/.env` (asset, non versionné) | | Vues | **2** — recherche (`SearchScreen`) + profil (`ProfileScreen`) | | Bonus | **Refresh token** sur 401 + retry des requêtes API | **Point d’entrée :** [`swiftyCompanion/swifty_companion/lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) → `home: SearchScreen()`. --- ## 2. Chapitre IV — Instructions générales & sécurité | Exigence sujet | Réalisation | Fichiers | |----------------|-------------|----------| | Langage mobile (Flutter cité) | Projet Flutter standard | `swifty_companion/pubspec.yaml` | | Dernière API 42 | Base URL v2 | [`lib/config/intra_config.dart`](../../swiftyCompanion/swifty_companion/lib/config/intra_config.dart) — `apiBaseUrl` | | Secrets en **`.env`**, **pas dans Git** | `CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URI` | `.env` (local) · modèle [`swiftyCompanion/.env.example`](../../swiftyCompanion/.env.example) · ignore [`swiftyCompanion/.gitignore`](../../swiftyCompanion/.gitignore) et [`.gitignore` racine travail](../../.gitignore) | | Chargement `.env` au démarrage | `dotenv.load` | [`lib/main.dart`](../../swiftyCompanion/swifty_companion/lib/main.dart) + asset dans `pubspec.yaml` (`assets: - .env`) | **À dire en correction :** les clés OAuth ne sont jamais dans le code source ; l’évaluateur configure son propre `.env` à partir de `.env.example`. --- ## 3. Chapitre V.1 — Partie obligatoire (9 critères) ### Critère 1 — Au moins 2 vues | Vue | Rôle | Fichier | |-----|------|---------| | **Vue 1** | Connexion API + recherche par login | [`lib/screens/search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) | | **Vue 2** | Profil étudiant | [`lib/screens/profile_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/profile_screen.dart) | **Démo :** montrer explicitement les deux écrans après une recherche réussie. --- ### Critère 2 — Gérer tous les cas d’erreur | Situation | Message utilisateur (ex.) | Où c’est géré | |-----------|---------------------------|---------------| | Pas connecté OAuth | « Connecte-toi d’abord à l’API 42 » | [`intra_api_exception.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_api_exception.dart) — `IntraNotAuthenticatedException` · [`intra_user_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_user_service.dart) | | Login vide | « Saisis un login » | `IntraUserService.fetchUserByLogin` | | Login inconnu | « Aucun utilisateur pour ce login » | `IntraUserNotFoundException` | | Pas de réseau | « Connexion impossible. Vérifie le réseau. » | `IntraNetworkException` · `SocketException` / `ClientException` dans user + profile services | | OAuth annulé / échec | Messages OAuth / plateforme | [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_connect()` | | Session expirée (401) | « Session expirée. Reconnecte-toi… » | `IntraUnauthorizedException` · [`intra_http.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_http.dart) | | Accès refusé (403) | Scopes `public` + `projects` | `IntraForbiddenException` | | Trop de requêtes (429) | Message + attente / retry | `IntraRateLimitException` · retry dans `intra_http.dart` | | Erreur profil (réseau, API) | Message + bouton **Réessayer** | `profile_screen.dart` — `_ErrorBody` | | `.env` manquant | Texte explicite au lancement | `search_screen.dart` — branche `!IntraConfig.isConfigured` | **Démo suggérée :** login `zzzzinvalid` → mode avion → (optionnel) profil en erreur + réessai. --- ### Critère 3 — Vue 2 : infos du login s’il existe - Navigation **uniquement** si l’API renvoie un utilisateur : [`search_screen.dart`](../../swiftyCompanion/swifty_companion/lib/screens/search_screen.dart) — `_search()` → `Navigator.push(ProfileScreen(user: …))`. - Si liste vide → exception, **pas** d’ouverture du profil. --- ### Critère 4 — ≥ 4 détails + photo de profil | Donnée affichée | Source API | Fichier UI | |-----------------|------------|------------| | **Login** | `user['login']` | AppBar + en-tête profil — `profile_screen.dart` | | **Photo** | `image.link`, `image.versions`, `image_url` | `_imageUrl` + `_IntraProfileAvatar` (`http.get` + Bearer via `cdnImageHeaders`) — `profile_screen.dart` | | **Email** | `user['email']` | `_DetailsCard` | | **Téléphone** | `user['phone']` | `_DetailsCard` | | **Niveau cursus** | `cursus_users` (slug `42`) | `IntraProfileService` + `_DetailsCard` | | **Localisation** | `user['location']` | `_DetailsCard` | | **Wallet** | `user['wallet']` | `_DetailsCard` | Chargement profil : [`lib/services/intra_profile_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_profile_service.dart) — `GET /users/{login}`, puis `projects_users`. **Note sujet :** champs masqués par l’API → affichage `—` (confidentialité OAuth). **Non affiché (optionnel sujet) :** `evaluation_points` — tu as déjà **> 4** champs sans lui. --- ### Critère 5 — Skills : niveau + pourcentage - **Pas** `/experiences` (souvent 403). - Skills depuis **`cursus_users[].skills`** sur la fiche user. - Niveau entier = partie entière du `level` API ; **%** = partie décimale × 100. - Parsing : `IntraProfileService._parseSkillsFromCursus` · affichage : `_SkillsCard` + barre de progression. **Démo :** lire une ligne du type `Algorithms — niveau 8 (42 %)`. --- ### Critère 6 — Projets réalisés, y compris failed - `GET /users/{id}/projects_users?page[size]=100` - Filtre app : statuts **`finished`** et **`failed`** — `_parseProjects` dans `intra_profile_service.dart`. - Liste : `_ProjectsCard` (libellés Validé / Échec + note si présente). **Scope OAuth requis :** `projects` (sinon 403). --- ### Critère 7 — Revenir à la vue 1 - `MaterialPageRoute` + **retour système** / flèche AppBar sur le profil. - `ProfileScreen` : `SwiftyUi.appBar(..., showBack: true)`. - Session OAuth **conservée** sur `SearchScreen` (pas de nouveau login). **Démo :** retour → nouvelle recherche sans « Se connecter avec 42 ». --- ### Critère 8 — Layout flexible / moderne | Mécanisme | Fichier | |-----------|---------| | Scroll + clavier | `ResponsiveScrollBody` — [`lib/theme/swifty_ui.dart`](../../swiftyCompanion/swifty_companion/lib/theme/swifty_ui.dart) | | Formulaire centré (vue 1) | `searchMaxWidth` 480 px | | Profil 2 colonnes si large | `wideBreakpoint` 720 px — `profile_screen.dart` `LayoutBuilder` | | Lignes détail empilées si étroit | `_DetailRow` si largeur < 400 px | | Thème 42 turquoise | `SwiftyUi.brand` · `main.dart` `ColorScheme.fromSeed` | **Démo :** rotation paysage ou grand écran → 2 colonnes sur le profil. --- ### Critère 9 — Un token OAuth, pas un token par requête | Règle | Implémentation | |-------|----------------| | Un seul login OAuth | `IntraAuthService.instance` — [`intra_auth_service.dart`](../../swiftyCompanion/swifty_companion/lib/services/intra_auth_service.dart) | | Bearer sur chaque GET | `authHeaders()` | | Pas de OAuth à chaque recherche | `IntraUserService` / `IntraProfileService` réutilisent le même service | **À dire :** un flux `authorizeAndExchangeCode` au début ; ensuite uniquement des `GET` API avec le même access token. --- ## 4. OAuth2 Intra (encadrés sujet) | Élément | Valeur / choix | Fichiers | |---------|----------------|----------| | Authorize | `https://api.intra.42.fr/oauth/authorize` | `intra_config.dart` | | Token | `https://api.intra.42.fr/oauth/token` | `intra_config.dart` + refresh dans `intra_auth_service.dart` | | Scopes | `public`, `projects` | `intra_auth_service.dart` — `AuthorizationTokenRequest` | | Redirect (device) | `com.example.swiftycompanion://callback` | `.env` · [`intra-oauth-formulaire.md`](intra-oauth-formulaire.md) | | Android redirect | `RedirectUriReceiverActivity` | [`android/app/src/main/AndroidManifest.xml`](../../swiftyCompanion/swifty_companion/android/app/src/main/AndroidManifest.xml) | | Gradle scheme | `appAuthRedirectScheme` | [`android/app/build.gradle.kts`](../../swiftyCompanion/swifty_companion/android/app/build.gradle.kts) | **Doc perso (non rendue Intra) :** [etape-02-oauth-intra.md](etape-02-oauth-intra.md), [oauth-test-manuel.md](oauth-test-manuel.md). --- ## 5. Chapitre V.2 — Bonus (refresh token) | Exigence | Réalisation | Fichiers | |----------|-------------|----------| | Stocker refresh au login | `result.refreshToken` | `intra_auth_service.dart` — `signIn()` | | Rafraîchir à expiration | `POST` `grant_type=refresh_token` | `refreshAccessToken()` | | App continue | Sur **401** → refresh → **rejouer** le GET | `intra_http.dart` — `intraApiGet(..., auth:)` | | Parsing JSON token | | `intra_oauth_tokens.dart` + tests | | Test manuel debug | Bouton « DEBUG : simuler token expiré » | `search_screen.dart` (`kDebugMode` uniquement) puis **Rechercher** | **Limite assumée :** tokens en **mémoire** seulement — après kill de l’app, reconnexion OAuth (acceptable si tu l’expliques). **Guide détaillé :** [etape-07-bonus-token.md](etape-07-bonus-token.md). --- ## 6. Inventaire des fichiers applicatifs (lib/) Fichiers **métier** à connaître pour la soutenance : ``` swiftyCompanion/swifty_companion/lib/ ├── main.dart # App + thème + dotenv ├── config/ │ └── intra_config.dart # URLs + variables .env ├── services/ │ ├── intra_auth_service.dart # OAuth, access + refresh │ ├── intra_user_service.dart # Recherche login │ ├── intra_profile_service.dart # Profil agrégé │ ├── intra_http.dart # GET + 401/429 │ ├── intra_oauth_tokens.dart # Parse token JSON (bonus) │ └── intra_api_exception.dart # Erreurs métier ├── screens/ │ ├── search_screen.dart # Vue 1 │ └── profile_screen.dart # Vue 2 └── theme/ └── swifty_ui.dart # Layout partagé (étape 5) ``` **Tests :** ``` test/ ├── widget_test.dart # Démarre app + bouton connexion └── intra_oauth_tokens_test.dart # Parse refresh (bonus) ``` **Fichier supprimé (nettoyage) :** `auth_screen.dart` (écran test obsolète, non utilisé). **Plateforme Android (OAuth physique) :** `AndroidManifest.xml`, `build.gradle.kts` (voir §4). --- ## 7. Endpoints API utilisés | Usage | Méthode | Appelé depuis | |-------|---------|---------------| | Session courante | `GET /v2/me` | `IntraAuthService.fetchMe` (après login) | | Recherche | `GET /v2/users?filter[login]=` | `IntraUserService` | | Fiche complète | `GET /v2/users/{login}` | `IntraProfileService` | | Cursus (secours) | `GET /v2/users/{id}/cursus_users` | `IntraProfileService` si pas embarqué | | Projets | `GET /v2/users/{id}/projects_users` | `IntraProfileService` | | Refresh token | `POST /oauth/token` | `IntraAuthService.refreshAccessToken` | Requêtes **séquentielles** + cache profil 2 min + retry 429 : voir [etape-04-vue-profil.md](etape-04-vue-profil.md). --- ## 8. Chapitre VI — Rendu Git | À pousser sur Vogosphere | À ne **pas** pousser | |--------------------------|----------------------| | Contenu de `swiftyCompanion/` | Dossier `docs/` (dépôt perso) | | `README.md`, `.gitignore`, `.env.example` | Fichier `.env` | | `swifty_companion/` (code Flutter) | `.env copy`, secrets dans l’historique Git | Checklist : [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) · [swiftyCompanion/README.md](../../swiftyCompanion/README.md). --- ## 9. Script de démo (5–8 min) 1. `cd swiftyCompanion/swifty_companion` → `flutter test` (rapide). 2. `flutter run -d ` — montrer `.env` configuré **sans** afficher les secrets. 3. **Se connecter avec 42** — une fois. 4. Recherche login valide → **profil** : photo, champs, skills, projet failed si dispo. 5. Retour → **deuxième** recherche (même session). 6. Login invalide → message. 7. Mode avion → erreur réseau. 8. **Bonus :** DEBUG simuler token expiré → **Rechercher** → OK sans OAuth navigateur. 9. Rappeler : 2 vues, 1 token, `.env` ignoré par git. --- ## 10. Parcours pédagogique (docs perso uniquement) | Étape | Guide | |-------|--------| | 0–1 | [00-reprise-et-etat.md](00-reprise-et-etat.md) | | 2 OAuth | [etape-02-oauth-intra.md](etape-02-oauth-intra.md) | | 3 Recherche | [etape-03-vue-recherche.md](etape-03-vue-recherche.md) | | 4 Profil | [etape-04-vue-profil.md](etape-04-vue-profil.md) | | 5 Layout | [etape-05-navigation-layout.md](etape-05-navigation-layout.md) | | 6 Soutenance | [etape-06-checklist-soutenance.md](etape-06-checklist-soutenance.md) | | 7 Bonus | [etape-07-bonus-token.md](etape-07-bonus-token.md) | | **Ce document** | **Cartographie sujet ↔ code pour la correction** | --- ## 11. Checklist « je n’ai rien oublié » (avant peer-eval) - [ ] App OAuth Intra : scopes **public** + **projects**, redirect = `.env` - [ ] `.env` présent sur la machine de démo, **absent** du Git école - [ ] `flutter analyze` / `flutter test` OK - [ ] Parcours nominal + erreurs + retour testés sur **device** - [ ] Bonus refresh testé (DEBUG → Rechercher) - [ ] Prêt à expliquer **où** est chaque critère du sujet (ce guide §3) - [ ] SECRET régénéré si jamais exposé (chat, commit accidentel) - [ ] Fichier **`.env copy`** retiré du Git — voir [audit-conformite-usage-api.md](audit-conformite-usage-api.md) --- *Dernière alignement : sujet PDF v4 — traduction [swifty-companion.md](../sujets/swifty-companion.md).*