@ollaid/native-sso 2.1.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,6 +28,7 @@ Package NPM Frontend-First pour l'authentification Native SSO Ollaid.
28
28
  19. [Sécurité](#sécurité)
29
29
  20. [Exports](#exports)
30
30
  21. [Publication & Installation npm](#publication--installation-npm)
31
+ 22. [Webhooks & Health Check (Backend)](#webhooks--health-check-backend)
31
32
 
32
33
  ---
33
34
 
@@ -59,7 +60,7 @@ function App() {
59
60
  return (
60
61
  <Routes>
61
62
  <Route
62
- path="/auth/sso"
63
+ path="/auth/native-sso"
63
64
  element={
64
65
  <NativeSSOPage
65
66
  saasApiUrl="https://mon-saas.com/api"
@@ -69,7 +70,7 @@ function App() {
69
70
  console.log('Connecté !', user.name);
70
71
  navigate('/dashboard');
71
72
  }}
72
- onLogout={() => navigate('/auth/sso')}
73
+ onLogout={() => navigate('/auth/native-sso')}
73
74
  />
74
75
  }
75
76
  />
@@ -81,7 +82,7 @@ function App() {
81
82
 
82
83
  ### 3. C'est tout ✅
83
84
 
84
- La page `/auth/sso` gère automatiquement :
85
+ La page `/auth/native-sso` gère automatiquement :
85
86
  - ✅ Connexion par email (mot de passe + OTP)
86
87
  - ✅ Connexion par téléphone (SMS OTP)
87
88
  - ✅ Connexion par code d'accès
@@ -104,15 +105,16 @@ La page `/auth/sso` gère automatiquement :
104
105
  | `configPrefix` | `string` | ❌ | **Multi-tenant** : préfixe de configuration IAM côté backend (défaut: `'iam'`). Permet à un même backend SaaS de gérer N applications IAM. Voir [Multi-Tenant](#multi-tenant-plusieurs-applications-sur-le-même-backend). |
105
106
  | `onLoginSuccess` | `(token: string, user: UserInfos) => void` | ❌ | Callback après connexion réussie |
106
107
  | `onLogout` | `() => void` | ❌ | Callback après déconnexion |
107
- | `title` | `string` | ❌ | Titre personnalisé (défaut: "Un compte, plusieurs accès") |
108
+ | `title` | `string` | ❌ | Titre personnalisé (défaut: "Un compte pour toutes vos applications") |
108
109
  | `description` | `string` | ❌ | Description personnalisée |
109
110
  | `logoUrl` | `string` | ❌ | URL du logo (remplace le slider) |
110
111
  | `hideFooter` | `boolean` | ❌ | Masquer "Propulsé par iam.ollaid.com" |
111
- | `onOnboardingComplete` | `(data: { image_url?: string; ccphone?: string; phone?: string }) => void` | ❌ | Callback après complétion de l'onboarding |
112
+ | `onOnboardingComplete` | `(data: { name?: string; image_url?: string; ccphone?: string; phone?: string; email?: string }) => void` | ❌ | Callback après complétion de l'onboarding |
112
113
  | `redirectAfterLogin` | `string` | ❌ | Route vers laquelle rediriger après connexion réussie (ex: `/client/dashboard`). Utilise `window.location.href`. Compatible avec ou sans react-router. |
113
114
  | `redirectAfterLogout` | `string` | ❌ | Route vers laquelle rediriger après déconnexion (ex: `/auth/client`). Utilise `window.location.href`. |
114
115
 
115
116
  > **Note :** Le mode `debug` est contrôlé **uniquement** par le backend via la variable d'environnement `IAM_DEBUG` dans le `.env` du SaaS. Il n'y a plus de prop `debug` à passer au composant. Le `DebugPanel` est **réactif** : il apparaît automatiquement après le chargement des credentials si `debug: true` est retourné par le backend.
117
+ > Quand le `DebugPanel` est visible, il expose aussi des boutons de test pour ouvrir `Connexion`, `Inscription`, `Infos profile` et un reset du rappel profil.
116
118
 
117
119
  ### Redirections automatiques (optionnel)
118
120
 
@@ -175,7 +177,7 @@ const handleLogout = async () => {
175
177
 
176
178
  1. **Révoque le token SaaS** — `POST /api/native/logout` (supprime le Sanctum token)
177
179
  2. **Révoque la session IAM** — `POST /api/iam/disconnect` (avec `sanctum_token` + `app_access_token_ref`)
178
- 3. **Nettoie le localStorage** — supprime les 6 clés du package
180
+ 3. **Nettoie le localStorage** — supprime les clés de session du package
179
181
 
180
182
  Les appels réseau sont en `Promise.allSettled` (best-effort) : même si le serveur est injoignable, le localStorage est **toujours** nettoyé.
181
183
 
@@ -189,6 +191,9 @@ Les appels réseau sont en `Promise.allSettled` (best-effort) : même si le serv
189
191
  | `account_type` | Type de compte (`user` ou `client`) |
190
192
  | `alias_reference` | Référence de l'alias de connexion |
191
193
  | `app_access_token_ref` | Référence de l'`AppAccessToken` IAM (pour revocation optimisée) |
194
+ | `refresh_token` | Refresh token SaaS (si activé) |
195
+ | `token_expires_at` | Expiration du token Sanctum (si fournie) |
196
+ | `refresh_expires_at` | Expiration du refresh token (si fournie) |
192
197
 
193
198
  ### ⛔ `clearAuthToken()` est déprécié
194
199
 
@@ -235,7 +240,7 @@ Backend SaaS:
235
240
  configPrefix="iam"
236
241
  accountType="user"
237
242
  redirectAfterLogin="/dashboard"
238
- redirectAfterLogout="/auth/sso"
243
+ redirectAfterLogout="/auth/native-sso"
239
244
  />
240
245
 
241
246
  {/* Page login espace vendeur */}
@@ -329,7 +334,7 @@ return [
329
334
 
330
335
  ### Côté Backend SaaS — Controller multi-tenant
331
336
 
332
- Tous les controllers Native (`config`, `exchange`, `check-token`, `logout`) doivent lire le header :
337
+ Tous les controllers Native (`config`, `exchange`, `check-token`, `refresh`, `logout`) doivent lire le header :
333
338
 
334
339
  ```php
335
340
  class NativeConfigController extends Controller
@@ -374,7 +379,11 @@ class NativeConfigController extends Controller
374
379
  }
375
380
  ```
376
381
 
377
- > **⚠️ Important** : Appliquez la même logique `X-IAM-Config-Prefix` dans `exchange`, `check-token` et `logout`.
382
+ > **⚠️ Important** : Appliquez la même logique `X-IAM-Config-Prefix` dans `exchange`, `check-token`, `refresh` et `logout`.
383
+
384
+ Le package envoie aussi :
385
+ - `X-Device-Id` (stable par appareil / webview)
386
+ - `X-Session-UUID` (UUID stable par instance, pour différencier plusieurs sessions sur un même device)
378
387
 
379
388
  ---
380
389
 
@@ -618,7 +627,8 @@ import { useMobileRegistration } from '@ollaid/native-sso';
618
627
 
619
628
  ## Backend SaaS — Endpoints requis
620
629
 
621
- Le backend SaaS (Laravel) doit exposer **4 endpoints**. Voici les spécifications exactes :
630
+ Le backend SaaS (Laravel) doit exposer **5 endpoints** : `config`, `exchange`, `check-token`, `refresh`, `logout`.
631
+ Voici les spécifications exactes :
622
632
 
623
633
  ### `GET /api/native/config`
624
634
 
@@ -638,7 +648,7 @@ Retourne un **token opaque chiffré** (`encrypted_credentials`) contenant les cr
638
648
  ```
639
649
 
640
650
  **Principe :**
641
- Le SaaS chiffre `app_key + secret_key + timestamp` en AES-256-CBC en utilisant la `secret_key` elle-même comme clé de chiffrement (SHA-256 → 32 bytes).
651
+ Le SaaS chiffre `secret_key + timestamp` en AES-256-CBC avec `OPENSSL_RAW_DATA`, en utilisant la `secret_key` elle-même comme clé de chiffrement (SHA-256 → 32 bytes).
642
652
  Le frontend transporte le blob opaque + l'`app_key` en clair (non sensible) vers l'IAM.
643
653
  L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, puis déchiffre le blob.
644
654
 
@@ -648,24 +658,26 @@ L'IAM retrouve la `secret_key` via l'`app_key` dans sa table `applications`, pui
648
658
  Route::get('/native/config', function () {
649
659
  $appKey = config('services.iam.app_key');
650
660
  $secretKey = config('services.iam.secret_key');
661
+ $iamApiUrl = config('services.iam.api_url');
651
662
 
652
663
  // Clé AES = SHA-256 de la secret_key (32 bytes binaires)
653
- $aesKey = hash('sha256', $secretKey, true);
664
+ $encryptionKey = hash('sha256', $secretKey, true);
654
665
  $iv = random_bytes(16);
655
666
 
656
667
  $payload = json_encode([
657
- 'app_key' => $appKey,
658
668
  'secret_key' => $secretKey,
659
669
  'ts' => time(),
660
670
  ]);
661
671
 
662
- $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $aesKey, 0, $iv);
672
+ $encrypted = openssl_encrypt($payload, 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv);
663
673
 
664
674
  return response()->json([
665
- 'success' => true,
666
- 'app_key' => $appKey, // En clair (non sensible, sert d'identifiant)
667
- 'encrypted_credentials' => base64_encode($iv . '::' . $encrypted),
668
- 'debug' => (bool) config('services.iam.debug'),
675
+ 'success' => true,
676
+ 'app_key' => $appKey, // En clair (non sensible, sert d'identifiant)
677
+ 'encrypted_credentials' => base64_encode($iv . '::' . base64_encode($encrypted)),
678
+ 'iam_api_url' => $iamApiUrl,
679
+ 'credentials_ttl' => 300,
680
+ 'debug' => (bool) config('services.iam.debug'),
669
681
  ]);
670
682
  });
671
683
  ```
@@ -680,12 +692,12 @@ if (!$app) {
680
692
  }
681
693
 
682
694
  // 2. Déchiffrer avec la secret_key de l'application
683
- $aesKey = hash('sha256', $app->secret_key, true); // secret_key = colonne DB
695
+ $encryptionKey = hash('sha256', $app->secret_key, true);
684
696
  $decoded = base64_decode($request->encrypted_credentials);
685
- [$iv, $ciphertext] = explode('::', $decoded, 2);
697
+ [$iv, $ciphertextB64] = explode('::', $decoded, 2);
686
698
 
687
699
  $payload = json_decode(
688
- openssl_decrypt($ciphertext, 'aes-256-cbc', $aesKey, 0, $iv),
700
+ openssl_decrypt(base64_decode($ciphertextB64), 'AES-256-CBC', $encryptionKey, OPENSSL_RAW_DATA, $iv),
689
701
  true
690
702
  );
691
703
 
@@ -694,7 +706,6 @@ if (!$payload || time() - $payload['ts'] > 300) {
694
706
  return response()->json(['success' => false, 'message' => 'Credentials expirés ou invalides'], 401);
695
707
  }
696
708
 
697
- $appKey = $payload['app_key'];
698
709
  $secretKey = $payload['secret_key'];
699
710
  // ... valider et continuer le flux
700
711
  ```
@@ -721,8 +732,8 @@ $secretKey = $payload['secret_key'];
721
732
 
722
733
  **Logique backend :**
723
734
  1. Recevoir le `callback_token`
724
- 2. Appeler l'IAM `POST /api/iam/auth/decrypt` avec `app_key`, `secret_key` et le `callback_token` (remplacer les espaces par `+`)
725
- 3. L'IAM retourne les `user_infos` (9 champs standardisés)
735
+ 2. Appeler l'IAM `POST /iam/auth/decrypt` avec les credentials dans les headers (`X-IAM-App-Key`, `X-IAM-Secret-Key`) et le token dans le body
736
+ 3. L'IAM retourne les `user_infos` (9 champs standardisés) + `app_access_token_ref`
726
737
  4. Créer ou mettre à jour l'utilisateur local
727
738
  5. Générer un token Sanctum
728
739
  6. Retourner le token + user
@@ -753,16 +764,14 @@ $secretKey = $payload['secret_key'];
753
764
  ```php
754
765
  // routes/api.php
755
766
  Route::post('/native/exchange', function (Request $request) {
756
- $callbackToken = $request->input('callback_token');
767
+ $iamApiUrl = config('services.iam.api_url');
757
768
 
758
- // ⚠️ IMPORTANT : Remplacer les espaces par '+' avant décryptage
759
- $callbackToken = str_replace(' ', '+', $callbackToken);
760
-
761
- // Appel IAM pour décrypter
762
- $response = Http::post(config('services.iam.base_url') . '/iam/auth/decrypt', [
763
- 'app_key' => config('services.iam.app_key'),
764
- 'secret_key' => config('services.iam.secret_key'),
765
- 'callback_token' => $callbackToken,
769
+ // Appel IAM pour décrypter credentials dans les HEADERS
770
+ $response = Http::withHeaders([
771
+ 'X-IAM-App-Key' => config('services.iam.app_key'),
772
+ 'X-IAM-Secret-Key' => config('services.iam.secret_key'),
773
+ ])->post("{$iamApiUrl}/iam/auth/decrypt", [
774
+ 'token' => $request->callback_token,
766
775
  ]);
767
776
 
768
777
  if (!$response->successful() || !$response->json('success')) {
@@ -773,7 +782,9 @@ Route::post('/native/exchange', function (Request $request) {
773
782
  ], 401);
774
783
  }
775
784
 
776
- $userInfos = $response->json('user_infos');
785
+ $data = $response->json();
786
+ $userInfos = $data['user_infos'];
787
+ $appAccessTokenRef = $data['app_access_token_ref'] ?? null;
777
788
 
778
789
  // Créer ou mettre à jour l'utilisateur local
779
790
  $user = User::updateOrCreate(
@@ -786,16 +797,13 @@ Route::post('/native/exchange', function (Request $request) {
786
797
  'image' => $userInfos['image_url'] ?? null,
787
798
  'alias_reference' => $userInfos['alias_reference'],
788
799
  'user_infos' => json_encode($userInfos),
789
- 'password' => bcrypt(Str::random(32)), // Mot de passe aléatoire
800
+ 'password' => bcrypt(Str::random(32)),
790
801
  ]
791
802
  );
792
803
 
793
804
  // Générer un token Sanctum
794
805
  $token = $user->createToken('native-sso')->plainTextToken;
795
806
 
796
- // Récupérer app_access_token_ref depuis la réponse IAM
797
- $appAccessTokenRef = $response->json('user_infos.app_access_token_ref');
798
-
799
807
  return response()->json([
800
808
  'success' => true,
801
809
  'token' => $token,
@@ -828,6 +836,7 @@ Vérifie la validité du token Sanctum et retourne les infos utilisateur fraîch
828
836
  **Réponse succès (200) :**
829
837
  ```json
830
838
  {
839
+ "success": true,
831
840
  "status": "connected",
832
841
  "user": {
833
842
  "name": "John Doe",
@@ -851,6 +860,7 @@ Route::post('/native/check-token', function (Request $request) {
851
860
  $user = $request->user();
852
861
 
853
862
  return response()->json([
863
+ 'success' => true,
854
864
  'status' => 'connected',
855
865
  'user' => [
856
866
  'name' => $user->name,
@@ -866,7 +876,46 @@ Route::post('/native/check-token', function (Request $request) {
866
876
  })->middleware('auth:sanctum');
867
877
  ```
868
878
 
869
- > **Comportement réseau :** Le package ne déconnecte l'utilisateur que si le backend retourne explicitement **401**. En cas d'erreur réseau, timeout ou serveur inaccessible, la session est conservée.
879
+ > **Règle de déconnexion :** Le package ne déconnecte l'utilisateur **que** sur un **HTTP 401 explicite**. Tout HTTP 200 (quelle que soit la structure du body) confirme la session. Erreur réseau, timeout, 5xx, offline → session conservée, jamais de déconnexion inutile.
880
+
881
+ ---
882
+
883
+ ### `POST /api/native/refresh` (OBLIGATOIRE pour la stabilité)
884
+
885
+ Renouvelle la session **sans déconnecter**. Le package l'utilise :
886
+ - en **proactif** (avant expiration, si `expires_at` est fourni)
887
+ - en **récupération** quand `check-token` retourne `401` (tente un refresh avant logout)
888
+
889
+ **Body :**
890
+ ```json
891
+ { "refresh_token": "rt_..." }
892
+ ```
893
+
894
+ **Recommandation (stabilité maximale) :** ne pas changer le token Sanctum.
895
+ Le refresh doit **prolonger `expires_at`** du token existant (le client garde son token actuel).
896
+
897
+ **Réponse succès (200) :**
898
+ ```json
899
+ {
900
+ "success": true,
901
+ "expires_at": "2026-06-13T12:00:00+00:00",
902
+ "refresh_token": "rt_new_...",
903
+ "refresh_expires_at": "2026-08-13T12:00:00+00:00",
904
+ "user": { "name": "John Doe" }
905
+ }
906
+ ```
907
+
908
+ **Réponse refresh invalide (401) :**
909
+ ```json
910
+ {
911
+ "success": false,
912
+ "error_type": "invalid_refresh",
913
+ "message": "Refresh token invalide ou expiré"
914
+ }
915
+ ```
916
+
917
+ > Si le refresh est invalide (`401 invalid_refresh`) : c'est une révocation explicite, la déconnexion est autorisée.
918
+ > Si offline/timeout/5xx : ne jamais déconnecter.
870
919
 
871
920
  ---
872
921
 
@@ -902,7 +951,7 @@ Route::post('/native/logout', function (Request $request) {
902
951
 
903
952
  ## Controller Laravel Complet (copier-coller)
904
953
 
905
- Voici un `NativeAuthController.php` complet regroupant les 4 endpoints. Copiez-le dans `app/Http/Controllers/Api/NativeAuthController.php` :
954
+ Voici un `NativeAuthController.php` complet regroupant les 5 endpoints. Copiez-le dans `app/Http/Controllers/Api/NativeAuthController.php` :
906
955
 
907
956
  ```php
908
957
  <?php
@@ -1124,6 +1173,7 @@ class NativeAuthController extends Controller
1124
1173
  $user = $request->user();
1125
1174
 
1126
1175
  return response()->json([
1176
+ 'success' => true,
1127
1177
  'status' => 'connected',
1128
1178
  'message' => 'Utilisateur connecté',
1129
1179
  'user' => [
@@ -1290,7 +1340,7 @@ Ajoutez ces variables dans le fichier `.env` du backend SaaS :
1290
1340
  # Credentials IAM (récupérés depuis le dashboard iam.ollaid.com)
1291
1341
  IAM_APP_KEY=oiam_ak_xxxxxxxxxxxxxxxxxxxxxxxx
1292
1342
  IAM_SECRET_KEY=oiam_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1293
- IAM_BASE_URL=https://identityam.ollaid.com/api
1343
+ IAM_API_URL=https://identityam.ollaid.com/api
1294
1344
 
1295
1345
  # Mode debug — contrôle le DebugPanel et les logs côté frontend
1296
1346
  # true : active le DebugPanel + logs console + détails d'erreur dans les réponses JSON
@@ -1307,7 +1357,7 @@ Et dans `config/services.php` :
1307
1357
  'iam' => [
1308
1358
  'app_key' => env('IAM_APP_KEY'),
1309
1359
  'secret_key' => env('IAM_SECRET_KEY'),
1310
- 'base_url' => env('IAM_BASE_URL', 'https://identityam.ollaid.com/api'),
1360
+ 'api_url' => env('IAM_API_URL', 'https://identityam.ollaid.com/api'),
1311
1361
  'debug' => env('IAM_DEBUG', false), // Active le debug côté frontend (false si absent)
1312
1362
  ],
1313
1363
  ```
@@ -1342,6 +1392,14 @@ public function up(): void
1342
1392
 
1343
1393
  > **Note :** Le champ `reference` est l'identifiant unique IAM de l'utilisateur. Le champ `alias_reference` identifie l'utilisateur dans le contexte d'une application spécifique.
1344
1394
 
1395
+ ### Colonnes recommandées sur `personal_access_tokens` (Sanctum)
1396
+
1397
+ Pour la révocation synchronisée et la stabilité de session :
1398
+ - `app_access_token_ref` (révocation IAM ciblée)
1399
+ - `refresh_token_hash` + `refresh_expires_at` (refresh token)
1400
+
1401
+ Ces migrations sont détaillées dans `BACKEND_INTEGRATION.md` du package.
1402
+
1345
1403
  ---
1346
1404
 
1347
1405
  ## Flux d'authentification
@@ -1386,7 +1444,7 @@ public function up(): void
1386
1444
  4. **Validate** — Le frontend envoie le mot de passe/OTP, l'IAM retourne un `callback_token`
1387
1445
  5. **Exchange** — Le frontend envoie le `callback_token` au backend SaaS, qui le décrypte via l'IAM et crée une session Sanctum
1388
1446
 
1389
- > **Important :** Le package gère les étapes 1-5 automatiquement. Le backend SaaS doit implémenter **4 endpoints** (`config`, `exchange`, `check-token`, `logout`).
1447
+ > **Important :** Le package gère les étapes 1-5 automatiquement. Le backend SaaS doit implémenter **5 endpoints** (`config`, `exchange`, `check-token`, `refresh`, `logout`).
1390
1448
 
1391
1449
  ---
1392
1450
 
@@ -1896,7 +1954,7 @@ Toutes les APIs IAM retournent le même objet `user_infos` avec exactement **9 c
1896
1954
 
1897
1955
  ## Session & localStorage
1898
1956
 
1899
- Le package utilise **6 clés** dans `localStorage` pour persister la session :
1957
+ Le package utilise **9 clés** dans `localStorage` pour persister la session et le suivi du profil :
1900
1958
 
1901
1959
  | Clé | Contenu | Source | Valeurs possibles |
1902
1960
  |-----|---------|--------|-------------------|
@@ -1906,8 +1964,12 @@ Le package utilise **6 clés** dans `localStorage` pour persister la session :
1906
1964
  | `account_type` | Type de compte | Déterminé lors du `exchange` selon le mode d'inscription | `"user"` (défaut) ou `"client"` (inscription phone-only) |
1907
1965
  | `alias_reference` | Référence alias utilisée lors de la connexion | Réponse de `/api/native/exchange` (champ `user.alias_reference`) | `"ALI-XXXXXXXX"` |
1908
1966
  | `app_access_token_ref` | Référence de l'`AppAccessToken` IAM | Réponse de `/api/native/exchange` (champ `app_access_token_ref`) | `"42"` ou `"aat_ref_abc123"` |
1967
+ | `sso_image_last_status` | Statut du dernier onboarding profil | Mis à jour par `OnboardingModal` | `"true"` ou `"false"` |
1968
+ | `sso_image_last_check` | Timestamp du dernier contrôle profil | Mis à jour par `OnboardingModal` / sync profil | Millisecondes Unix |
1969
+ | `sso_image_recheck_at` | Timestamp de rappel après snooze | Mis à jour quand l'utilisateur passe l'onboarding | Millisecondes Unix |
1909
1970
 
1910
1971
  > **Note :** `account_type` et `alias_reference` sont stockés **séparément** de l'objet `user` en plus d'être inclus dans le JSON de la clé `user`.
1972
+ > **Note :** `sso_image_last_status`, `sso_image_last_check` et `sso_image_recheck_at` vivent dans le `localStorage` du SaaS et servent au rappel d'onboarding du profil.
1911
1973
 
1912
1974
  #### Champs enrichis dans l'objet `user`
1913
1975
 
@@ -1917,7 +1979,8 @@ L'objet `user` stocké en localStorage contient deux champs ajoutés automatique
1917
1979
 
1918
1980
  ### Nettoyage
1919
1981
 
1920
- Lors du `logout()`, les 6 clés sont supprimées via `clearAuthToken()`.
1982
+ Lors du `logout()`, les 6 clés de session sont supprimées via `clearAuthToken()`.
1983
+ Les clés de rappel profil (`sso_image_*`) ne sont pas effacées, afin de conserver le comportement de relance après snooze.
1921
1984
 
1922
1985
  ### Accès programmatique
1923
1986
 
@@ -2054,7 +2117,7 @@ Le [health check](#usetokenhealthcheck) (toutes les 2 min) détecte automatiquem
2054
2117
 
2055
2118
  ---
2056
2119
 
2057
- Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil.
2120
+ Modal post-connexion qui invite l'utilisateur à compléter les informations manquantes de son profil, ou à éditer ses informations complètes depuis un SaaS.
2058
2121
 
2059
2122
  ### Props
2060
2123
 
@@ -2063,35 +2126,102 @@ Modal post-connexion qui invite l'utilisateur à compléter les informations man
2063
2126
  | `open` | `boolean` | ✅ | Contrôle l'ouverture de la modal |
2064
2127
  | `onOpenChange` | `(open: boolean) => void` | ✅ | Callback changement d'état |
2065
2128
  | `user` | `NativeUser` | ✅ | Objet utilisateur courant (pour détecter les champs manquants) |
2129
+ | `variant` | `'missing' \| 'edit'` | ❌ | `missing` affiche uniquement les champs absents, `edit` affiche les champs de base en mode édition complet |
2066
2130
  | `onComplete` | `(data) => void` | ✅ | Callback avec les données saisies |
2067
2131
  | `onSkip` | `() => void` | ✅ | Callback si l'utilisateur passe l'étape |
2068
2132
 
2069
2133
  ### Champs affichés
2070
2134
 
2071
- La modal affiche **uniquement** les champs manquants :
2072
- - **Photo de profil** — si `user.image_url` est vide (max 2 Mo, JPG/PNG)
2073
- - **Numéro de téléphone** si `user.phone` est vide
2074
- - **Adresse email** — si `user.email` est vide (**optionnel**, ne bloque pas la validation)
2135
+ La modal gère **deux modes** :
2136
+
2137
+ - **Mode `missing`** : affiche uniquement les champs manquants
2138
+ - **Nom complet** — si `user.name` est vide
2139
+ - **Photo de profil** — si `user.image_url` est vide (max 2 Mo, JPG/PNG)
2140
+ - **Numéro de téléphone** — si `user.phone` est vide
2141
+ - **Adresse email** — si `user.email` est vide
2142
+ - **Mode `edit`** : affiche les champs de base éditables même si le profil est déjà complet
2143
+ - Nom complet
2144
+ - Photo de profil
2145
+ - Numéro de téléphone en lecture seule avec bouton `Changer téléphone`
2146
+ - Adresse email en lecture seule avec bouton `Changer email`
2075
2147
 
2076
2148
  ### Callback `onComplete`
2077
2149
 
2078
2150
  ```ts
2079
2151
  onComplete: (data: {
2152
+ name?: string; // Nom complet (si fourni ou modifié)
2080
2153
  image_url?: string; // Base64 de la photo (si ajoutée)
2081
2154
  ccphone?: string; // Indicatif (si téléphone ajouté)
2082
2155
  phone?: string; // Numéro (si téléphone ajouté)
2083
- email?: string; // Email (si renseigné — optionnel)
2156
+ email?: string; // Email (si renseigné)
2084
2157
  }) => void;
2085
2158
  ```
2086
2159
 
2087
2160
  ### Condition de soumission
2088
2161
 
2089
2162
  L'utilisateur **doit** :
2090
- 1. Cocher la case de confirmation
2091
- 2. Fournir une photo (si manquante)
2092
- 3. Fournir un téléphone valide (si manquant)
2163
+ 1. Fournir un nom si le nom est affiché
2164
+ 2. Fournir une photo si elle est affichée
2165
+ 3. Fournir un téléphone valide si le champ est affiché
2166
+ 4. Fournir un email valide si le champ est affiché
2167
+
2168
+ Après connexion, la page autonome attend 5 minutes avant d'ouvrir cette modal si le profil est incomplet.
2169
+ Si l'utilisateur la ferme sans compléter, le package enregistre un snooze de 8 heures (`sso_image_last_status=false`, `sso_image_last_check=now`, `sso_image_recheck_at=now+8h`).
2170
+ Un sync profil best-effort relit ensuite l'état utilisateur toutes les 30 minutes et remet `sso_image_last_status=true` dès que le profil est complet.
2171
+
2172
+ ### Mode automatique
2173
+
2174
+ Le mode automatique est piloté par `NativeSSOPage` :
2175
+ - la modal s'ouvre après le délai de 5 minutes si des infos sont manquantes,
2176
+ - elle reste sur un écran de chargement tant que l'hydratation IAM n'a pas renvoyé le profil,
2177
+ - elle affiche ensuite uniquement les champs manquants,
2178
+ - si l'utilisateur ferme la modal, le rappel est repoussé de 8 heures.
2179
+
2180
+ ### Mode manuel depuis un SaaS
2181
+
2182
+ Quand vous ouvrez la modal depuis votre SaaS via un bouton "Infos profil" ou "Modifier mon profil", utilisez `variant="edit"`.
2093
2183
 
2094
- L'email est **toujours optionnel** — il n'est pas requis pour valider.
2184
+ Exemple :
2185
+
2186
+ ```tsx
2187
+ import { useState } from 'react';
2188
+ import { OnboardingModal } from '@ollaid/native-sso';
2189
+
2190
+ export function ProfileButton({ user }: { user: NativeUser }) {
2191
+ const [open, setOpen] = useState(false);
2192
+
2193
+ return (
2194
+ <>
2195
+ <button type="button" onClick={() => setOpen(true)}>
2196
+ Infos profil
2197
+ </button>
2198
+
2199
+ <OnboardingModal
2200
+ open={open}
2201
+ onOpenChange={setOpen}
2202
+ onDismiss={() => setOpen(false)}
2203
+ user={user}
2204
+ variant="edit"
2205
+ profileHydrating={false}
2206
+ onComplete={(data) => {
2207
+ // Mettre à jour votre cache local / localStorage avec data.user_infos
2208
+ setOpen(false);
2209
+ }}
2210
+ onSkip={() => setOpen(false)}
2211
+ />
2212
+ </>
2213
+ );
2214
+ }
2215
+ ```
2216
+
2217
+ Dans ce mode, le changement de téléphone/email ouvre un sous-flux OTP dans la même modal:
2218
+ 1. saisie du nouveau téléphone/email,
2219
+ 2. envoi du premier OTP sur le contact actuel,
2220
+ 3. vérification du premier OTP,
2221
+ 4. envoi d'un second OTP sur le nouveau contact,
2222
+ 5. confirmation finale et mise à jour du profil.
2223
+
2224
+ Les écrans de changement restent en `static backdrop` côté package: un clic hors modal ne ferme pas le flux.
2095
2225
 
2096
2226
  ### Exemple
2097
2227
 
@@ -2102,6 +2232,7 @@ import { OnboardingModal } from '@ollaid/native-sso';
2102
2232
  open={showOnboarding}
2103
2233
  onOpenChange={setShowOnboarding}
2104
2234
  user={currentUser}
2235
+ variant="edit"
2105
2236
  onComplete={async (data) => {
2106
2237
  // Envoyer les données au backend pour mise à jour
2107
2238
  await api.updateProfile(data);
@@ -2250,6 +2381,9 @@ if (config('services.iam.debug')) {
2250
2381
  - `getAuthToken` — Récupérer le token depuis localStorage
2251
2382
  - `getAuthUser` — Récupérer l'utilisateur depuis localStorage
2252
2383
  - `getAccountType` — Récupérer le type de compte depuis localStorage
2384
+ - `getDeviceId` — Récupérer/générer le `X-Device-Id` (persisté)
2385
+ - `getSessionUuid` — Récupérer/générer le `X-Session-UUID` (persisté)
2386
+ - `STORAGE_KEYS` — Constantes des clés localStorage utilisées par le package
2253
2387
 
2254
2388
  ### Types
2255
2389
  - `UserInfos`, `NativeAuthState`, `NativeAuthStatus`, `NativeCredentials`, etc.
@@ -2354,3 +2488,16 @@ npm publish --access public # 3. Publier sur npm
2354
2488
  ## Licence
2355
2489
 
2356
2490
  Propriétaire — Ollaid © 2026
2491
+
2492
+ ---
2493
+
2494
+ ## Webhooks & Health Check (Backend)
2495
+
2496
+ Pour garantir la fiabilité et la synchronisation en temps réel (ex: révocation instantanée d'une session bannie), votre backend doit implémenter deux briques supplémentaires.
2497
+
2498
+ Consultez le guide détaillé : [**WEBHOOKS_HEALTH.md**](./WEBHOOKS_HEALTH.md)
2499
+
2500
+ | Brique | Rôle |
2501
+ |--------|------|
2502
+ | **Health Check** | Permet à l'IAM de vérifier que votre SaaS est "Healthy" et bien configuré. |
2503
+ | **Webhooks** | Permet à l'IAM de notifier votre SaaS d'événements critiques (suspension, révocation). |
@@ -0,0 +1,16 @@
1
+ /**
2
+ * AvatarCropModal — recadrage carré simple et déterministe
3
+ *
4
+ * Version réécrite from scratch pour éviter les états qui bloquent le bouton Valider.
5
+ *
6
+ * @version 2.5.0
7
+ */
8
+ export interface AvatarCropModalProps {
9
+ open: boolean;
10
+ imageSrc: string | null;
11
+ onOpenChange: (open: boolean) => void;
12
+ onCancel: () => void;
13
+ onConfirm: (blob: Blob) => Promise<void> | void;
14
+ }
15
+ export declare function AvatarCropModal({ open, imageSrc, onOpenChange, onCancel, onConfirm }: AvatarCropModalProps): import("react/jsx-runtime").JSX.Element;
16
+ export default AvatarCropModal;
@@ -2,11 +2,16 @@
2
2
  * DebugPanel — Panneau de debug flottant pour @ollaid/native-sso
3
3
  * Affiche l'historique des appels API en temps réel (style terminal)
4
4
  * N'apparaît que quand debug=true
5
- * @version 1.0.0
5
+ * @version 2.5.0
6
6
  */
7
+ export type DebugOnboardingPreset = 'current' | 'photo' | 'phone' | 'email' | 'all';
7
8
  interface DebugPanelProps {
8
9
  saasApiUrl: string;
9
10
  iamApiUrl: string;
11
+ onOpenLogin?: () => void;
12
+ onOpenSignup?: () => void;
13
+ onOpenOnboarding?: (preset: DebugOnboardingPreset) => void;
14
+ onResetProfilePrompt?: () => void;
10
15
  }
11
- export declare function DebugPanel({ saasApiUrl, iamApiUrl }: DebugPanelProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function DebugPanel({ saasApiUrl, iamApiUrl, onOpenLogin, onOpenSignup, onOpenOnboarding, onResetProfilePrompt }: DebugPanelProps): import("react/jsx-runtime").JSX.Element;
12
17
  export default DebugPanel;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Login Modal for @ollaid/native-sso
3
- * Complete login flow aligned with web SSO /sso/auth design
3
+ * Complete login flow aligned with Native SSO design
4
4
  *
5
- * @version 1.0.0
5
+ * @version 2.5.0
6
6
  */
7
7
  import type { UserInfos } from '../types/native';
8
8
  export interface LoginModalProps {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * NativeSSOPage — Page autonome complète pour @ollaid/native-sso
3
- * Design aligné sur /sso/auth (fond primary, card blanche, ShieldCheck branding)
3
+ * Design aligné sur le parcours Native SSO (fond primary, card blanche, ShieldCheck branding)
4
4
  *
5
- * @version 2.0.0
5
+ * @version 2.5.0
6
6
  */
7
7
  import type { UserInfos } from '../types/native';
8
8
  export interface NativeSSOPageProps {
@@ -11,9 +11,15 @@ export interface NativeSSOPageProps {
11
11
  onLoginSuccess?: (token: string, user: UserInfos) => void;
12
12
  onLogout?: () => void;
13
13
  onOnboardingComplete?: (data: {
14
+ name?: string;
14
15
  image_url?: string;
15
16
  ccphone?: string;
16
17
  phone?: string;
18
+ email?: string;
19
+ address?: string;
20
+ town?: string;
21
+ country?: string;
22
+ user_infos?: UserInfos;
17
23
  }) => void;
18
24
  accountType?: 'user' | 'client';
19
25
  configPrefix?: string;
@@ -1,23 +1,30 @@
1
1
  /**
2
- * OnboardingModal — Post-login modal for missing profile info
3
- * Asks for photo + phone if not present in user_infos
2
+ * OnboardingModal — Modal de profil unifiée
3
+ * Mode `missing` : champs absents uniquement
4
+ * Mode `edit` : édition complète du profil depuis le SaaS
4
5
  *
5
- * @version 1.0.0
6
+ * @version 2.5.0
6
7
  */
7
- import type { NativeUser } from '../types/native';
8
+ import type { NativeUser, UserInfos } from '../types/native';
8
9
  export interface OnboardingModalProps {
9
10
  open: boolean;
10
11
  onOpenChange: (open: boolean) => void;
12
+ onDismiss: () => void;
11
13
  user: NativeUser;
12
- /** Called with updated data when user submits */
14
+ variant?: 'missing' | 'edit';
15
+ profileHydrating?: boolean;
13
16
  onComplete: (data: {
17
+ name?: string;
14
18
  image_url?: string;
15
19
  ccphone?: string;
16
20
  phone?: string;
17
21
  email?: string;
22
+ address?: string;
23
+ town?: string;
24
+ country?: string;
25
+ user_infos?: UserInfos;
18
26
  }) => void;
19
- /** Called when user skips onboarding */
20
27
  onSkip: () => void;
21
28
  }
22
- export declare function OnboardingModal({ open, onOpenChange, user, onComplete, onSkip }: OnboardingModalProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function OnboardingModal({ open, onOpenChange, onDismiss, user, variant, profileHydrating, onComplete, onSkip }: OnboardingModalProps): import("react/jsx-runtime").JSX.Element;
23
30
  export default OnboardingModal;