@umituz/web-dashboard 3.2.4 → 4.1.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.
Files changed (126) hide show
  1. package/README.md +228 -14
  2. package/package.json +23 -30
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +43 -42
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +29 -87
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +69 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/index.ts +1 -0
  93. package/src/domains/settings/utils/settings.ts +11 -4
  94. package/src/i18n/I18nContext.tsx +33 -0
  95. package/src/i18n/defaultT.test.ts +43 -0
  96. package/src/i18n/defaultT.ts +34 -0
  97. package/src/i18n/index.ts +7 -0
  98. package/src/i18n/types.ts +13 -0
  99. package/src/index.ts +1 -7
  100. package/src/primitives/Button.tsx +24 -0
  101. package/src/primitives/Checkbox.tsx +17 -0
  102. package/src/primitives/Input.tsx +23 -0
  103. package/src/primitives/Skeleton.tsx +6 -0
  104. package/src/primitives/cn.ts +10 -0
  105. package/src/primitives/index.ts +23 -0
  106. package/src/styles/dashboard.css +2731 -0
  107. package/src/styles/tokens.contract.test.ts +91 -0
  108. package/src/styles/tokens.css +116 -0
  109. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  110. package/src/domains/billing/components/BillingPage.tsx +0 -49
  111. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  112. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  113. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  114. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  115. package/src/domains/billing/components/UsageCard.tsx +0 -103
  116. package/src/domains/billing/components/index.ts +0 -13
  117. package/src/domains/billing/constants/billing.ts +0 -69
  118. package/src/domains/billing/hooks/index.ts +0 -7
  119. package/src/domains/billing/hooks/useBilling.ts +0 -385
  120. package/src/domains/billing/index.ts +0 -73
  121. package/src/domains/billing/types/billing.ts +0 -342
  122. package/src/domains/billing/types/index.ts +0 -30
  123. package/src/domains/billing/utils/billing.ts +0 -360
  124. package/src/domains/billing/utils/index.ts +0 -31
  125. package/src/domains/billing/utils/polar.ts +0 -63
  126. package/src/domains/calendar/components/index.ts +0 -7
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SocialLoginButtons
3
+ *
4
+ * Renders a list of social auth providers as a row of icon buttons.
5
+ * Generic over the provider id so adding a new provider is a config change,
6
+ * not a code change.
7
+ */
8
+
9
+ import { Button } from "../../../../primitives";
10
+ import { GoogleIcon, AppleIcon } from "./SocialProviderIcons";
11
+
12
+ export type SocialProviderId = 'google' | 'apple';
13
+
14
+ export interface SocialLoginButtonsProps {
15
+ /** Provider handlers keyed by provider id */
16
+ onProvider: (id: SocialProviderId) => void;
17
+ /** Translation function */
18
+ translate: (key: string) => string;
19
+ /** Disable all buttons (e.g., during form submission) */
20
+ disabled?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Centralized provider config. Add new providers here and the UI follows.
25
+ */
26
+ const SOCIAL_PROVIDERS: ReadonlyArray<{
27
+ id: SocialProviderId;
28
+ labelKey: string;
29
+ Icon: typeof GoogleIcon;
30
+ }> = [
31
+ { id: 'google', labelKey: 'auth.social.continueWithGoogle', Icon: GoogleIcon },
32
+ { id: 'apple', labelKey: 'auth.social.continueWithApple', Icon: AppleIcon },
33
+ ];
34
+
35
+ export const SocialLoginButtons = ({
36
+ onProvider,
37
+ translate,
38
+ disabled = false,
39
+ }: SocialLoginButtonsProps) => {
40
+ return (
41
+ <div className="udb-social-grid">
42
+ {SOCIAL_PROVIDERS.map(({ id, labelKey, Icon }) => (
43
+ <Button
44
+ key={id}
45
+ type="button"
46
+ variant="outline"
47
+ onClick={() => onProvider(id)}
48
+ disabled={disabled}
49
+ aria-label={translate(labelKey)}
50
+ className="udb-social-grid__btn"
51
+ >
52
+ <Icon className="udb-social-grid__icon" aria-hidden="true" />
53
+ {translate(labelKey)}
54
+ </Button>
55
+ ))}
56
+ </div>
57
+ );
58
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Social Provider Icons
3
+ *
4
+ * Reusable SVG icon components for social auth providers.
5
+ * Centralizes the brand mark assets so consuming forms
6
+ * don't embed raw SVG paths in JSX.
7
+ */
8
+
9
+ import type { FC } from "react";
10
+
11
+ interface IconProps {
12
+ className?: string;
13
+ "aria-hidden"?: boolean | "true" | "false";
14
+ }
15
+
16
+ const baseProps = {
17
+ xmlns: "http://www.w3.org/2000/svg",
18
+ viewBox: "0 0 24 24",
19
+ fill: "currentColor",
20
+ "aria-hidden": true as const,
21
+ };
22
+
23
+ export const GoogleIcon: FC<IconProps> = ({ className, ...rest }) => (
24
+ <svg className={className} {...baseProps} {...rest}>
25
+ <path
26
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
27
+ fill="#4285F4"
28
+ />
29
+ <path
30
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
31
+ fill="#34A853"
32
+ />
33
+ <path
34
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
35
+ fill="#FBBC05"
36
+ />
37
+ <path
38
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
39
+ fill="#EA4335"
40
+ />
41
+ </svg>
42
+ );
43
+
44
+ export const AppleIcon: FC<IconProps> = ({ className, ...rest }) => (
45
+ <svg className={className} {...baseProps} {...rest}>
46
+ <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
47
+ </svg>
48
+ );
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared auth form components — barrel export
3
+ */
4
+
5
+ export { AuthErrorBanner } from './AuthErrorBanner';
6
+ export type { AuthErrorBannerProps } from './AuthErrorBanner';
7
+
8
+ export { EmailInput } from './EmailInput';
9
+ export type { EmailInputProps } from './EmailInput';
10
+
11
+ export { PasswordInput } from './PasswordInput';
12
+ export type { PasswordInputProps } from './PasswordInput';
13
+
14
+ export { PasswordStrengthIndicator } from './PasswordStrengthIndicator';
15
+ export type { PasswordStrengthIndicatorProps } from './PasswordStrengthIndicator';
16
+
17
+ export { SocialLoginButtons } from './SocialLoginButtons';
18
+ export type { SocialLoginButtonsProps, SocialProviderId } from './SocialLoginButtons';
19
+
20
+ export { GoogleIcon, AppleIcon } from './SocialProviderIcons';
@@ -5,3 +5,4 @@
5
5
  */
6
6
 
7
7
  export { useAuth } from "./useAuth";
8
+ export type { AuthProvider } from "./useAuth";
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * useAuth Hook
3
3
  *
4
- * Core authentication hook for managing auth state and actions
4
+ * Core authentication hook for managing auth state and actions.
5
+ * Centralizes the try/catch+loading+error pattern via a single helper
6
+ * to keep individual actions focused on their unique logic.
5
7
  */
6
8
 
7
- import { useState, useCallback, useEffect } from "react";
9
+ import { useState, useCallback } from "react";
8
10
  import type {
9
11
  AuthState,
10
12
  User,
@@ -21,278 +23,195 @@ import {
21
23
  validateResetPassword,
22
24
  } from "../utils/auth";
23
25
 
26
+ /**
27
+ * Auth provider contract — single source of truth for backend integration.
28
+ */
29
+ export interface AuthProvider {
30
+ login: (credentials: LoginCredentials) => Promise<User>;
31
+ register: (data: RegisterData) => Promise<User>;
32
+ logout: () => Promise<void>;
33
+ forgotPassword: (data: ForgotPasswordData) => Promise<void>;
34
+ resetPassword: (data: ResetPasswordData) => Promise<void>;
35
+ refreshAuth: () => Promise<User | null>;
36
+ }
37
+
24
38
  interface UseAuthOptions {
25
- /** Initial authenticated state */
26
- initialAuthenticated?: boolean;
27
- /** Current user data */
28
- initialUser?: User | null;
29
39
  /** Auth implementation (connects to your backend) */
30
- authProvider?: {
31
- login: (credentials: LoginCredentials) => Promise<User>;
32
- register: (data: RegisterData) => Promise<User>;
33
- logout: () => Promise<void>;
34
- forgotPassword: (data: ForgotPasswordData) => Promise<void>;
35
- resetPassword: (data: ResetPasswordData) => Promise<void>;
36
- refreshAuth: () => Promise<User | null>;
37
- };
40
+ authProvider?: AuthProvider;
38
41
  }
39
42
 
40
43
  /**
41
- * useAuth hook
44
+ * Wrap a side-effectful async function with the standard
45
+ * isLoading + error + throw contract.
42
46
  *
43
- * Manages authentication state and provides auth actions
47
+ * Important: the wrapped function is the SINGLE point of truth for
48
+ * setting isLoading=false, ensuring errors are surfaced (not swallowed),
49
+ * and re-throwing for callers that need to react.
50
+ */
51
+ const executeAuthAction = async <T>(
52
+ updateState: (updates: Partial<AuthState>) => void,
53
+ fallbackErrorMessage: string,
54
+ action: () => Promise<T>,
55
+ ): Promise<T> => {
56
+ updateState({ isLoading: true, error: null });
57
+ try {
58
+ return await action();
59
+ } catch (error) {
60
+ const errorMessage =
61
+ error instanceof Error && error.message ? error.message : fallbackErrorMessage;
62
+ updateState({ isLoading: false, error: errorMessage });
63
+ throw error instanceof Error ? error : new Error(errorMessage);
64
+ }
65
+ };
66
+
67
+ /**
68
+ * useAuth hook
44
69
  *
45
- * @param options - Hook options
46
- * @returns Auth state and actions
70
+ * Manages authentication state and provides auth actions.
71
+ * Each action surfaces errors to the caller AND to the auth state.
47
72
  */
48
73
  export function useAuth(options: UseAuthOptions = {}) {
49
- const {
50
- initialAuthenticated = false,
51
- initialUser = null,
52
- authProvider,
53
- } = options;
74
+ const { authProvider } = options;
54
75
 
55
- // State
56
76
  const [authState, setAuthState] = useState<AuthState>({
57
- isAuthenticated: initialAuthenticated,
58
- user: initialUser,
77
+ isAuthenticated: false,
78
+ user: null,
59
79
  isLoading: false,
60
80
  error: null,
61
81
  });
62
82
 
63
- // Update auth state helper
64
83
  const updateState = useCallback((updates: Partial<AuthState>) => {
65
84
  setAuthState((prev) => ({ ...prev, ...updates }));
66
85
  }, []);
67
86
 
68
- // Login action
69
- const login = useCallback(async (credentials: LoginCredentials) => {
70
- // Validate credentials
71
- const validation = validateLogin(credentials);
72
- if (!validation.valid) {
73
- updateState({ error: validation.error });
74
- throw new Error(validation.error);
87
+ const requireProvider = useCallback((): AuthProvider => {
88
+ if (!authProvider) {
89
+ throw new Error("Auth provider not configured");
75
90
  }
76
-
77
- updateState({ isLoading: true, error: null });
78
-
79
- try {
80
- if (!authProvider) {
81
- throw new Error("Auth provider not configured");
91
+ return authProvider;
92
+ }, [authProvider]);
93
+
94
+ const login = useCallback(
95
+ async (credentials: LoginCredentials): Promise<User> => {
96
+ const validation = validateLogin(credentials);
97
+ if (!validation.valid) {
98
+ const message = validation.error ?? "Login validation failed";
99
+ updateState({ error: message });
100
+ throw new Error(message);
82
101
  }
83
102
 
84
- const user = await authProvider.login(credentials);
85
- updateState({
86
- isAuthenticated: true,
87
- user,
88
- isLoading: false,
89
- error: null,
90
- });
91
-
92
- return user;
93
- } catch (error) {
94
- const errorMessage = error instanceof Error ? error.message : "Login failed";
95
- updateState({
96
- isAuthenticated: false,
97
- user: null,
98
- isLoading: false,
99
- error: errorMessage,
103
+ return executeAuthAction(updateState, "Login failed", async () => {
104
+ const provider = requireProvider();
105
+ const user = await provider.login(credentials);
106
+ updateState({ isAuthenticated: true, user, isLoading: false, error: null });
107
+ return user;
100
108
  });
101
- throw error;
102
- }
103
- }, [authProvider, updateState]);
104
-
105
- // Register action
106
- const register = useCallback(async (data: RegisterData) => {
107
- // Validate registration data
108
- const validation = validateRegister(data, false);
109
- if (!validation.valid) {
110
- updateState({ error: validation.error });
111
- throw new Error(validation.error);
112
- }
113
-
114
- updateState({ isLoading: true, error: null });
115
-
116
- try {
117
- if (!authProvider) {
118
- throw new Error("Auth provider not configured");
109
+ },
110
+ [requireProvider, updateState],
111
+ );
112
+
113
+ const register = useCallback(
114
+ async (data: RegisterData): Promise<User> => {
115
+ const validation = validateRegister(data, false);
116
+ if (!validation.valid) {
117
+ const message = validation.error ?? "Registration validation failed";
118
+ updateState({ error: message });
119
+ throw new Error(message);
119
120
  }
120
121
 
121
- const user = await authProvider.register(data);
122
- updateState({
123
- isAuthenticated: true,
124
- user,
125
- isLoading: false,
126
- error: null,
127
- });
128
-
129
- return user;
130
- } catch (error) {
131
- const errorMessage = error instanceof Error ? error.message : "Registration failed";
132
- updateState({
133
- isAuthenticated: false,
134
- user: null,
135
- isLoading: false,
136
- error: errorMessage,
122
+ return executeAuthAction(updateState, "Registration failed", async () => {
123
+ const provider = requireProvider();
124
+ const user = await provider.register(data);
125
+ updateState({ isAuthenticated: true, user, isLoading: false, error: null });
126
+ return user;
137
127
  });
138
- throw error;
139
- }
140
- }, [authProvider, updateState]);
141
-
142
- // Logout action
143
- const logout = useCallback(async () => {
144
- updateState({ isLoading: true, error: null });
145
-
146
- try {
147
- if (!authProvider) {
148
- throw new Error("Auth provider not configured");
128
+ },
129
+ [requireProvider, updateState],
130
+ );
131
+
132
+ const logout = useCallback(async (): Promise<void> => {
133
+ await executeAuthAction(updateState, "Logout failed", async () => {
134
+ const provider = requireProvider();
135
+ await provider.logout();
136
+ updateState({ isAuthenticated: false, user: null, isLoading: false, error: null });
137
+ });
138
+ }, [requireProvider, updateState]);
139
+
140
+ const forgotPassword = useCallback(
141
+ async (data: ForgotPasswordData): Promise<void> => {
142
+ const validation = validateForgotPassword(data);
143
+ if (!validation.valid) {
144
+ const message = validation.error ?? "Forgot password validation failed";
145
+ updateState({ error: message });
146
+ throw new Error(message);
149
147
  }
150
148
 
151
- await authProvider.logout();
152
-
153
- updateState({
154
- isAuthenticated: false,
155
- user: null,
156
- isLoading: false,
157
- error: null,
158
- });
159
- } catch (error) {
160
- const errorMessage = error instanceof Error ? error.message : "Logout failed";
161
- updateState({
162
- isLoading: false,
163
- error: errorMessage,
149
+ await executeAuthAction(updateState, "Failed to send reset email", async () => {
150
+ const provider = requireProvider();
151
+ await provider.forgotPassword(data);
152
+ updateState({ isLoading: false });
164
153
  });
165
- throw error;
166
- }
167
- }, [authProvider, updateState]);
168
-
169
- // Forgot password action
170
- const forgotPassword = useCallback(async (data: ForgotPasswordData) => {
171
- // Validate email
172
- const validation = validateForgotPassword(data);
173
- if (!validation.valid) {
174
- updateState({ error: validation.error });
175
- throw new Error(validation.error);
176
- }
177
-
178
- updateState({ isLoading: true, error: null });
179
-
180
- try {
181
- if (!authProvider) {
182
- throw new Error("Auth provider not configured");
154
+ },
155
+ [requireProvider, updateState],
156
+ );
157
+
158
+ const resetPassword = useCallback(
159
+ async (data: ResetPasswordData): Promise<void> => {
160
+ const validation = validateResetPassword(data);
161
+ if (!validation.valid) {
162
+ const message = validation.error ?? "Reset password validation failed";
163
+ updateState({ error: message });
164
+ throw new Error(message);
183
165
  }
184
166
 
185
- await authProvider.forgotPassword(data);
186
- updateState({ isLoading: false });
187
- } catch (error) {
188
- const errorMessage = error instanceof Error ? error.message : "Failed to send reset email";
189
- updateState({
190
- isLoading: false,
191
- error: errorMessage,
167
+ await executeAuthAction(updateState, "Failed to reset password", async () => {
168
+ const provider = requireProvider();
169
+ await provider.resetPassword(data);
170
+ updateState({ isLoading: false });
192
171
  });
193
- throw error;
194
- }
195
- }, [authProvider, updateState]);
196
-
197
- // Reset password action
198
- const resetPassword = useCallback(async (data: ResetPasswordData) => {
199
- // Validate reset data
200
- const validation = validateResetPassword(data);
201
- if (!validation.valid) {
202
- updateState({ error: validation.error });
203
- throw new Error(validation.error);
204
- }
205
-
206
- updateState({ isLoading: true, error: null });
207
-
208
- try {
209
- if (!authProvider) {
210
- throw new Error("Auth provider not configured");
172
+ },
173
+ [requireProvider, updateState],
174
+ );
175
+
176
+ const updateProfile = useCallback(
177
+ async (data: Partial<User>): Promise<User> => {
178
+ const currentUser = authState.user;
179
+ if (!currentUser) {
180
+ const message = "No user to update";
181
+ updateState({ error: message });
182
+ throw new Error(message);
211
183
  }
212
184
 
213
- await authProvider.resetPassword(data);
214
- updateState({ isLoading: false });
215
- } catch (error) {
216
- const errorMessage = error instanceof Error ? error.message : "Failed to reset password";
217
- updateState({
218
- isLoading: false,
219
- error: errorMessage,
220
- });
221
- throw error;
222
- }
223
- }, [authProvider, updateState]);
224
-
225
- // Update profile action
226
- const updateProfile = useCallback(async (data: Partial<User>) => {
227
- updateState({ isLoading: true, error: null });
228
-
229
- try {
230
- // Update local user state
231
- let updatedUser: User | null = null;
232
- setAuthState((prev) => {
233
- updatedUser = prev.user ? { ...prev.user, ...data } : null;
234
- return {
235
- ...prev,
236
- user: updatedUser,
237
- isLoading: false,
238
- error: null,
185
+ return executeAuthAction(updateState, "Failed to update profile", async () => {
186
+ // Pure derivation of the new user, no side effects in setState callback.
187
+ // Required fields come from the existing user; data overrides only the rest.
188
+ const { id: _ignoredId, ...rest } = data;
189
+ const updatedUser: User = {
190
+ ...currentUser,
191
+ ...rest,
192
+ id: currentUser.id,
193
+ email: data.email ?? currentUser.email,
239
194
  };
195
+ updateState({ user: updatedUser, isLoading: false, error: null });
196
+ return updatedUser;
240
197
  });
241
-
242
- // In production, call your API here
243
- // await authProvider?.updateProfile(data);
244
-
245
- if (!updatedUser) {
246
- throw new Error("No user to update");
247
- }
248
-
249
- return updatedUser;
250
- } catch (error) {
251
- const errorMessage = error instanceof Error ? error.message : "Failed to update profile";
252
- updateState({
253
- isLoading: false,
254
- error: errorMessage,
255
- });
256
- throw error;
257
- }
258
- }, [updateState]);
259
-
260
- // Refresh auth action
261
- const refresh = useCallback(async () => {
262
- updateState({ isLoading: true, error: null });
263
-
264
- try {
265
- if (!authProvider) {
266
- throw new Error("Auth provider not configured");
267
- }
268
-
269
- const user = await authProvider.refreshAuth();
270
-
271
- updateState({
272
- isAuthenticated: !!user,
273
- user,
274
- isLoading: false,
275
- error: null,
276
- });
277
-
198
+ },
199
+ [authState.user, updateState],
200
+ );
201
+
202
+ const refresh = useCallback(async (): Promise<User | null> => {
203
+ return executeAuthAction(updateState, "Failed to refresh authentication", async () => {
204
+ const provider = requireProvider();
205
+ const user = await provider.refreshAuth();
206
+ updateState({ isAuthenticated: Boolean(user), user, isLoading: false, error: null });
278
207
  return user;
279
- } catch (error) {
280
- updateState({
281
- isAuthenticated: false,
282
- user: null,
283
- isLoading: false,
284
- error: null,
285
- });
286
- throw error;
287
- }
288
- }, [authProvider, updateState]);
208
+ });
209
+ }, [requireProvider, updateState]);
289
210
 
290
- // Clear error action
291
211
  const clearError = useCallback(() => {
292
212
  updateState({ error: null });
293
213
  }, [updateState]);
294
214
 
295
- // Auth actions object
296
215
  const authActions: AuthActions = {
297
216
  login,
298
217
  register,
@@ -14,9 +14,7 @@ export {
14
14
  } from "./components";
15
15
 
16
16
  // Hooks
17
- export {
18
- useAuth,
19
- } from "./hooks";
17
+ export { useAuth, type AuthProvider } from "./hooks";
20
18
 
21
19
  // Utils
22
20
  export {
@@ -33,8 +31,13 @@ export {
33
31
  maskEmail,
34
32
  generateResetToken,
35
33
  calculatePasswordStrength,
36
- getPasswordStrengthLabel,
34
+ getPasswordStrengthBand,
37
35
  sanitizeInput,
36
+ AUTH_KEYS,
37
+ AUTH_VALIDATION_KEYS,
38
+ type ValidationResult,
39
+ type PasswordStrengthBand,
40
+ type AuthValidationKey,
38
41
  } from "./utils";
39
42
 
40
43
  // Types
@@ -55,4 +58,5 @@ export type {
55
58
  RegisterFormProps,
56
59
  ForgotPasswordFormProps,
57
60
  ResetPasswordFormProps,
61
+ Translate,
58
62
  } from "./types";
@@ -6,6 +6,13 @@
6
6
 
7
7
  import type { ComponentType, ReactElement } from "react";
8
8
 
9
+ /**
10
+ * Translation function contract. Auth components stay i18n-agnostic;
11
+ * the consumer plugs in their i18n library of choice.
12
+ * Interpolation params follow the i18next convention (`{{placeholder}}`).
13
+ */
14
+ export type Translate = (key: string, params?: Record<string, unknown>) => string;
15
+
9
16
  /**
10
17
  * User credentials for login
11
18
  */
@@ -156,6 +163,10 @@ export interface AuthConfig {
156
163
  showSocialLogin?: boolean;
157
164
  /** Available social providers */
158
165
  socialProviders?: SocialProvider[];
166
+ /** Public home route (header "back" link). Falls back to `afterLoginRoute`. */
167
+ homeRoute?: string;
168
+ /** Footer links (rendered only when provided — no defaults that 404) */
169
+ footerLinks?: Array<{ label: string; href: string }>;
159
170
  /** Enable remember me */
160
171
  enableRememberMe?: boolean;
161
172
  /** Enable email verification */
@@ -192,6 +203,10 @@ export interface AuthLayoutProps {
192
203
  authState?: AuthState;
193
204
  /** Auth actions */
194
205
  authActions?: AuthActions;
206
+ /** Optional translation function for layout strings (falls back to embedded English) */
207
+ translate?: Translate;
208
+ /** Social provider login handler (receives the provider id) */
209
+ onSocialLogin?: (providerId: string) => void | Promise<void>;
195
210
  /** Children content */
196
211
  children?: React.ReactNode;
197
212
  }
@@ -212,7 +227,13 @@ export interface LoginFormProps {
212
227
  showRegisterLink?: boolean;
213
228
  /** Show social login buttons */
214
229
  showSocialLogin?: boolean;
215
- /** Custom login handler (receives credentials, returns user) - overrides mock auth */
230
+ /**
231
+ * Allow the built-in mock login (accepts ANY credentials) for demos.
232
+ * Defaults to false — without `onLoginAttempt` the form reports a
233
+ * configuration error instead of silently "logging in".
234
+ */
235
+ enableMockAuth?: boolean;
236
+ /** Custom login handler (receives credentials, returns user) */
216
237
  onLoginAttempt?: (credentials: LoginCredentials) => Promise<User>;
217
238
  /** On successful login (called after onLoginAttempt or mock auth succeeds) */
218
239
  onLoginSuccess?: (user: User) => void | Promise<void>;
@@ -258,8 +279,8 @@ export interface RegisterFormProps {
258
279
  export interface ForgotPasswordFormProps {
259
280
  /** Auth configuration */
260
281
  config: AuthConfig;
261
- /** On success */
262
- onSuccess?: () => void | Promise<void>;
282
+ /** Submit handler — receives the entered email and performs the reset request */
283
+ onSuccess?: (data: { email: string }) => void | Promise<void>;
263
284
  /** On error */
264
285
  onError?: (error: string) => void;
265
286
  /** Show back to login link */
@@ -274,8 +295,8 @@ export interface ResetPasswordFormProps {
274
295
  config: AuthConfig;
275
296
  /** Reset token */
276
297
  token: string;
277
- /** On success */
278
- onSuccess?: () => void | Promise<void>;
298
+ /** Submit handler — receives the token and the new password */
299
+ onSuccess?: (data: { token: string; password: string }) => void | Promise<void>;
279
300
  /** On error */
280
301
  onError?: (error: string) => void;
281
302
  }