@ollaid/native-sso 2.6.0 → 2.7.1

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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Version réécrite from scratch pour éviter les états qui bloquent le bouton Valider.
5
5
  *
6
- * @version 2.6.0
6
+ * @version 2.7.0
7
7
  */
8
8
  export interface AvatarCropModalProps {
9
9
  open: boolean;
@@ -2,7 +2,7 @@
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 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  export type DebugOnboardingPreset = 'current' | 'photo' | 'phone' | 'email' | 'all';
8
8
  interface DebugPanelProps {
@@ -2,7 +2,7 @@
2
2
  * Login Modal for @ollaid/native-sso
3
3
  * Complete login flow aligned with Native SSO design
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  import type { UserInfos } from '../types/native';
8
8
  export interface LoginModalProps {
@@ -2,7 +2,7 @@
2
2
  * NativeSSOPage — Page autonome complète pour @ollaid/native-sso
3
3
  * Design aligné sur le parcours Native SSO (fond primary, card blanche, ShieldCheck branding)
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  import type { UserInfos } from '../types/native';
8
8
  import type { NativeStorageAdapter } from '../services/api';
@@ -3,7 +3,7 @@
3
3
  * Mode `missing` : champs absents uniquement
4
4
  * Mode `edit` : édition complète du profil depuis le SaaS
5
5
  *
6
- * @version 2.6.0
6
+ * @version 2.7.0
7
7
  */
8
8
  import type { NativeUser, UserInfos } from '../types/native';
9
9
  export interface OnboardingModalProps {
@@ -3,7 +3,7 @@
3
3
  * Flow: email → method-choice → OTP → new password → success
4
4
  * Design aligned with web SSO
5
5
  *
6
- * @version 2.6.0
6
+ * @version 2.7.0
7
7
  */
8
8
  export interface PasswordRecoveryModalProps {
9
9
  open: boolean;
@@ -2,7 +2,7 @@
2
2
  * Signup Modal for @ollaid/native-sso — Design aligned with web SSO
3
3
  * Full signup flow: intro → account-type → info → OTP → password → confirm → success
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  import type { UserInfos } from '../types/native';
8
8
  export interface SignupModalProps {
@@ -3,7 +3,7 @@
3
3
  * Lightweight replacements for shadcn/ui components + inline SVG icons
4
4
  * No external dependencies required
5
5
  *
6
- * @version 2.6.0
6
+ * @version 2.7.0
7
7
  */
8
8
  import React from 'react';
9
9
  export declare function IconShieldCheck(props: React.SVGProps<SVGSVGElement>): import("react/jsx-runtime").JSX.Element;
@@ -22,7 +22,7 @@
22
22
  * };
23
23
  * ```
24
24
  *
25
- * @version 2.6.0
25
+ * @version 2.7.0
26
26
  */
27
27
  export interface UseLogoutOptions {
28
28
  /** Callback appelé après une déconnexion réussie (redirection, toast, etc.) */
@@ -2,7 +2,7 @@
2
2
  * Hook de récupération de mot de passe v1.0
3
3
  * Architecture Frontend-First avec appels directs à l'IAM
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  export interface UseMobilePasswordOptions {
8
8
  saasApiUrl: string;
@@ -2,7 +2,7 @@
2
2
  * Hook d'inscription Mobile SSO v1.0
3
3
  * Gère le flow: init → verify-otp → complete
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  import type { MobileRegistrationFormData, AccountType } from '../types/mobile';
8
8
  interface RegistrationConflict {
@@ -2,7 +2,7 @@
2
2
  * Hook d'authentification Native SSO v1.0
3
3
  * Architecture Frontend-First avec appels directs à l'IAM
4
4
  *
5
- * @version 2.6.0
5
+ * @version 2.7.0
6
6
  */
7
7
  import { type NativeStorageAdapter } from '../services/api';
8
8
  import type { NativeAuthStatus, NativeExchangeResponse, AccountType } from '../types/native';
@@ -10,7 +10,7 @@
10
10
  * - Si 401 → révoque l'IAM (POST /iam/disconnect) + nettoie le frontend
11
11
  * - Ne déconnecte PAS si offline ou serveur inaccessible
12
12
  *
13
- * @version 2.6.0
13
+ * @version 2.7.0
14
14
  */
15
15
  import type { UserInfos } from '../types/native';
16
16
  export interface UseTokenHealthCheckOptions {
package/dist/index.cjs CHANGED
@@ -291,17 +291,18 @@ function OTPInput({
291
291
  disabled = false,
292
292
  autoFocus = true
293
293
  }) {
294
+ const safeLength = Number.isFinite(length) ? Math.max(1, Math.min(12, Math.trunc(length))) : 6;
294
295
  const inputRefs = react.useRef([]);
295
296
  const [activeIndex, setActiveIndex] = react.useState(0);
296
297
  react.useEffect(() => {
297
- inputRefs.current = inputRefs.current.slice(0, length);
298
- }, [length]);
298
+ inputRefs.current = inputRefs.current.slice(0, safeLength);
299
+ }, [safeLength]);
299
300
  react.useEffect(() => {
300
301
  if (autoFocus && inputRefs.current[0]) inputRefs.current[0].focus();
301
302
  }, [autoFocus]);
302
303
  react.useEffect(() => {
303
- if (value.length === length && onComplete) onComplete(value);
304
- }, [value, length, onComplete]);
304
+ if (value.length === safeLength && onComplete) onComplete(value);
305
+ }, [value, safeLength, onComplete]);
305
306
  const handleChange = (index, char) => {
306
307
  var _a;
307
308
  if (disabled) return;
@@ -309,8 +310,8 @@ function OTPInput({
309
310
  if (!digit) return;
310
311
  const newValue = value.split("");
311
312
  newValue[index] = digit;
312
- onChange(newValue.join("").slice(0, length));
313
- if (index < length - 1) {
313
+ onChange(newValue.join("").slice(0, safeLength));
314
+ if (index < safeLength - 1) {
314
315
  (_a = inputRefs.current[index + 1]) == null ? void 0 : _a.focus();
315
316
  setActiveIndex(index + 1);
316
317
  }
@@ -333,7 +334,7 @@ function OTPInput({
333
334
  } else if (e.key === "ArrowLeft" && index > 0) {
334
335
  (_b = inputRefs.current[index - 1]) == null ? void 0 : _b.focus();
335
336
  setActiveIndex(index - 1);
336
- } else if (e.key === "ArrowRight" && index < length - 1) {
337
+ } else if (e.key === "ArrowRight" && index < safeLength - 1) {
337
338
  (_c = inputRefs.current[index + 1]) == null ? void 0 : _c.focus();
338
339
  setActiveIndex(index + 1);
339
340
  }
@@ -344,14 +345,14 @@ function OTPInput({
344
345
  e.preventDefault();
345
346
  const pasted = e.clipboardData.getData("text").replace(/[^0-9]/g, "");
346
347
  if (pasted) {
347
- const newValue = pasted.slice(0, length);
348
+ const newValue = pasted.slice(0, safeLength);
348
349
  onChange(newValue);
349
- const focusIndex = Math.min(newValue.length, length - 1);
350
+ const focusIndex = Math.min(newValue.length, safeLength - 1);
350
351
  (_a = inputRefs.current[focusIndex]) == null ? void 0 : _a.focus();
351
352
  setActiveIndex(focusIndex);
352
353
  }
353
354
  };
354
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: "0.5rem" }, children: Array.from({ length }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
355
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", gap: "0.5rem" }, children: Array.from({ length: safeLength }).map((_, index) => /* @__PURE__ */ jsxRuntime.jsx(
355
356
  "input",
356
357
  {
357
358
  ref: (el) => inputRefs.current[index] = el,
@@ -7557,8 +7558,12 @@ function getHeaders(token, includeConfigPrefix = false) {
7557
7558
  let credentials = null;
7558
7559
  let credentialsLoadedAt = 0;
7559
7560
  let credentialsTtl = 300;
7561
+ let refreshInFlight = null;
7562
+ let lastSuccessfulRefreshAt = 0;
7563
+ let lastSuccessfulRefreshResponse = null;
7560
7564
  const DEFAULT_TTL = 300;
7561
7565
  const REFRESH_MARGIN = 30;
7566
+ const REFRESH_COOLDOWN_MS = 60 * 1e3;
7562
7567
  const nativeAuthService = {
7563
7568
  hasCredentials() {
7564
7569
  return credentials !== null;
@@ -7857,6 +7862,16 @@ const nativeAuthService = {
7857
7862
  }
7858
7863
  },
7859
7864
  async refresh() {
7865
+ const now = Date.now();
7866
+ if (refreshInFlight) {
7867
+ return refreshInFlight;
7868
+ }
7869
+ if ((lastSuccessfulRefreshResponse == null ? void 0 : lastSuccessfulRefreshResponse.success) && lastSuccessfulRefreshAt && now - lastSuccessfulRefreshAt < REFRESH_COOLDOWN_MS) {
7870
+ if (isDebugMode()) {
7871
+ console.log("🔄 [SaaS] POST /native/refresh — cooldown actif, réponse réutilisée");
7872
+ }
7873
+ return lastSuccessfulRefreshResponse;
7874
+ }
7860
7875
  const cfg = getNativeAuthConfig();
7861
7876
  if (!cfg.saasApiUrl) {
7862
7877
  throw new ApiError("saasApiUrl non configurée", "unknown");
@@ -7869,51 +7884,62 @@ const nativeAuthService = {
7869
7884
  if (isDebugMode()) {
7870
7885
  console.log("📤 [SaaS] POST /native/refresh");
7871
7886
  }
7872
- let response;
7873
- try {
7874
- response = await fetchWithTimeout(
7875
- `${cfg.saasApiUrl}/native/refresh`,
7876
- {
7877
- method: "POST",
7878
- headers: getHeaders(void 0, true),
7879
- body: JSON.stringify({ refresh_token: refreshToken })
7880
- },
7881
- cfg.timeout || 3e4
7882
- );
7883
- } catch (err) {
7884
- if (err instanceof ApiError) {
7885
- if (err.statusCode === 401) {
7886
- return {
7887
- success: false,
7888
- error_type: err.errorType || "invalid_refresh",
7889
- message: err.message
7890
- };
7887
+ refreshInFlight = (async () => {
7888
+ try {
7889
+ let response;
7890
+ try {
7891
+ response = await fetchWithTimeout(
7892
+ `${cfg.saasApiUrl}/native/refresh`,
7893
+ {
7894
+ method: "POST",
7895
+ headers: getHeaders(void 0, true),
7896
+ body: JSON.stringify({ refresh_token: refreshToken })
7897
+ },
7898
+ cfg.timeout || 3e4
7899
+ );
7900
+ } catch (err) {
7901
+ if (err instanceof ApiError) {
7902
+ if (err.statusCode === 401) {
7903
+ response = {
7904
+ success: false,
7905
+ error_type: err.errorType || "invalid_refresh",
7906
+ message: err.message
7907
+ };
7908
+ } else if (err.statusCode === 404) {
7909
+ response = {
7910
+ success: false,
7911
+ error_type: "not_supported",
7912
+ message: "Endpoint refresh non disponible sur ce SaaS"
7913
+ };
7914
+ } else {
7915
+ throw err;
7916
+ }
7917
+ } else {
7918
+ throw err;
7919
+ }
7891
7920
  }
7892
- if (err.statusCode === 404) {
7893
- return {
7894
- success: false,
7895
- error_type: "not_supported",
7896
- message: "Endpoint refresh non disponible sur ce SaaS"
7897
- };
7921
+ if (response.success) {
7922
+ if (response.token) {
7923
+ setAuthToken(response.token);
7924
+ }
7925
+ const storage = getNativeStorage();
7926
+ if (response.expires_at) storage.setItem(STORAGE.TOKEN_EXPIRES_AT, response.expires_at);
7927
+ if (response.refresh_token) storage.setItem(STORAGE.REFRESH_TOKEN, response.refresh_token);
7928
+ if (response.refresh_expires_at) storage.setItem(STORAGE.REFRESH_EXPIRES_AT, response.refresh_expires_at);
7929
+ if (response.app_access_token_ref) storage.setItem(STORAGE.APP_ACCESS_TOKEN_REF, response.app_access_token_ref);
7930
+ if (response.alias_reference) storage.setItem(STORAGE.ALIAS_REFERENCE, response.alias_reference);
7931
+ if (response.user) {
7932
+ setAuthUser(response.user);
7933
+ }
7934
+ lastSuccessfulRefreshAt = Date.now();
7935
+ lastSuccessfulRefreshResponse = response;
7898
7936
  }
7937
+ return response;
7938
+ } finally {
7939
+ refreshInFlight = null;
7899
7940
  }
7900
- throw err;
7901
- }
7902
- if (response.success) {
7903
- if (response.token) {
7904
- setAuthToken(response.token);
7905
- }
7906
- const storage = getNativeStorage();
7907
- if (response.expires_at) storage.setItem(STORAGE.TOKEN_EXPIRES_AT, response.expires_at);
7908
- if (response.refresh_token) storage.setItem(STORAGE.REFRESH_TOKEN, response.refresh_token);
7909
- if (response.refresh_expires_at) storage.setItem(STORAGE.REFRESH_EXPIRES_AT, response.refresh_expires_at);
7910
- if (response.app_access_token_ref) storage.setItem(STORAGE.APP_ACCESS_TOKEN_REF, response.app_access_token_ref);
7911
- if (response.alias_reference) storage.setItem(STORAGE.ALIAS_REFERENCE, response.alias_reference);
7912
- if (response.user) {
7913
- setAuthUser(response.user);
7914
- }
7915
- }
7916
- return response;
7941
+ })();
7942
+ return refreshInFlight;
7917
7943
  },
7918
7944
  async logout(token) {
7919
7945
  const config2 = getNativeAuthConfig();
@@ -7962,11 +7988,17 @@ const nativeAuthService = {
7962
7988
  clearAuthToken();
7963
7989
  credentials = null;
7964
7990
  credentialsLoadedAt = 0;
7991
+ refreshInFlight = null;
7992
+ lastSuccessfulRefreshAt = 0;
7993
+ lastSuccessfulRefreshResponse = null;
7965
7994
  return { success: true };
7966
7995
  },
7967
7996
  clearCredentials() {
7968
7997
  credentials = null;
7969
7998
  credentialsLoadedAt = 0;
7999
+ refreshInFlight = null;
8000
+ lastSuccessfulRefreshAt = 0;
8001
+ lastSuccessfulRefreshResponse = null;
7970
8002
  },
7971
8003
  // ============================================
7972
8004
  // High-level methods