@abpjs/account 3.0.0 → 3.2.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.
@@ -10,6 +10,14 @@ export interface ChangePasswordFormProps {
10
10
  * Callback fired on password change error
11
11
  */
12
12
  onError?: (error: string) => void;
13
+ /**
14
+ * Whether to hide the current password field.
15
+ * When undefined, this is automatically determined based on the user's profile.
16
+ * Users without a password (e.g., social login users) don't need to enter current password.
17
+ *
18
+ * @since 3.1.0
19
+ */
20
+ hideCurrentPassword?: boolean;
13
21
  }
14
22
  /**
15
23
  * ChangePasswordForm - Password change form component
@@ -18,6 +26,7 @@ export interface ChangePasswordFormProps {
18
26
  * It provides a form for authenticated users to change their password.
19
27
  *
20
28
  * @since 1.1.0
29
+ * @since 3.1.0 - Added hideCurrentPassword prop for users without password (social login)
21
30
  *
22
31
  * @example
23
32
  * ```tsx
@@ -25,7 +34,10 @@ export interface ChangePasswordFormProps {
25
34
  * onSuccess={() => console.log('Password changed!')}
26
35
  * onError={(err) => console.error(err)}
27
36
  * />
37
+ *
38
+ * // For social login users (no current password needed)
39
+ * <ChangePasswordForm hideCurrentPassword={true} />
28
40
  * ```
29
41
  */
30
- export declare function ChangePasswordForm({ onSuccess, onError }: ChangePasswordFormProps): import("react/jsx-runtime").JSX.Element;
42
+ export declare function ChangePasswordForm({ onSuccess, onError, hideCurrentPassword: hideCurrentPasswordProp, }: ChangePasswordFormProps): import("react/jsx-runtime").JSX.Element;
31
43
  export default ChangePasswordForm;
@@ -24,6 +24,14 @@ export interface ManageProfileProps {
24
24
  * Custom tabs to add/replace default tabs
25
25
  */
26
26
  customTabs?: ProfileTab[];
27
+ /**
28
+ * Whether to hide the change password tab.
29
+ * When undefined, this is automatically determined based on the user's profile.
30
+ * External users (social login) don't see the change password tab by default.
31
+ *
32
+ * @since 3.1.0
33
+ */
34
+ hideChangePasswordTab?: boolean;
27
35
  }
28
36
  /**
29
37
  * ManageProfile - User profile management component
@@ -34,6 +42,7 @@ export interface ManageProfileProps {
34
42
  *
35
43
  * @since 1.1.0
36
44
  * @since 2.7.0 - Added changePasswordKey and personalSettingsKey static properties for component replacement system
45
+ * @since 3.1.0 - Added hideChangePasswordTab prop and loading state; external users don't see change password tab
37
46
  *
38
47
  * @example
39
48
  * ```tsx
@@ -41,9 +50,12 @@ export interface ManageProfileProps {
41
50
  * defaultTabIndex={0}
42
51
  * onTabChange={(index) => console.log('Tab changed to', index)}
43
52
  * />
53
+ *
54
+ * // Force hide change password tab
55
+ * <ManageProfile hideChangePasswordTab={true} />
44
56
  * ```
45
57
  */
46
- export declare function ManageProfile({ defaultTabIndex, onTabChange, customTabs, }: ManageProfileProps): import("react/jsx-runtime").JSX.Element;
58
+ export declare function ManageProfile({ defaultTabIndex, onTabChange, customTabs, hideChangePasswordTab: hideChangePasswordTabProp, }: ManageProfileProps): import("react/jsx-runtime").JSX.Element;
47
59
  export declare namespace ManageProfile {
48
60
  var changePasswordKey: "Account.ChangePasswordComponent";
49
61
  var personalSettingsKey: "Account.PersonalSettingsComponent";
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Authentication Flow Guard
3
+ * Translated from @abp/ng.account v3.1.0
4
+ *
5
+ * A route guard that checks if the authentication flow is internal (password-based).
6
+ * If using external auth (SSO/OAuth), it initiates the login flow and blocks navigation.
7
+ *
8
+ * In Angular, this implements CanActivate interface.
9
+ * In React, this is a function that can be used with react-router loaders/guards.
10
+ *
11
+ * @since 3.1.0
12
+ */
13
+ /**
14
+ * Authentication flow guard result
15
+ */
16
+ export interface AuthenticationFlowGuardResult {
17
+ /**
18
+ * Whether navigation is allowed
19
+ */
20
+ canActivate: boolean;
21
+ /**
22
+ * If navigation is blocked, the reason why
23
+ */
24
+ reason?: 'external_auth' | 'not_configured';
25
+ }
26
+ /**
27
+ * Options for the authentication flow guard
28
+ */
29
+ export interface AuthenticationFlowGuardOptions {
30
+ /**
31
+ * Whether internal authentication is being used.
32
+ * When true, the guard allows navigation.
33
+ * When false, the guard blocks navigation and initiates login.
34
+ */
35
+ isInternalAuth: boolean;
36
+ /**
37
+ * Function to initiate the login flow for external auth
38
+ */
39
+ initLogin: () => void | Promise<void>;
40
+ }
41
+ /**
42
+ * Check if navigation should be allowed based on auth flow type
43
+ *
44
+ * @param options - The guard options
45
+ * @returns Whether navigation is allowed
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * // In a route loader
50
+ * export const loader = async () => {
51
+ * const result = authenticationFlowGuard({
52
+ * isInternalAuth: true, // or false for SSO
53
+ * initLogin: () => auth.login(),
54
+ * });
55
+ *
56
+ * if (!result.canActivate) {
57
+ * return redirect('/');
58
+ * }
59
+ *
60
+ * return null;
61
+ * };
62
+ * ```
63
+ */
64
+ export declare function authenticationFlowGuard(options: AuthenticationFlowGuardOptions): AuthenticationFlowGuardResult;
65
+ /**
66
+ * React hook version of AuthenticationFlowGuard
67
+ *
68
+ * This hook can be used in a route component to check auth flow
69
+ * and redirect if needed.
70
+ *
71
+ * @param options - The guard options
72
+ * @returns Whether navigation is allowed
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * function ProtectedRoute({ children }) {
77
+ * const auth = useAuth();
78
+ * const canActivate = useAuthenticationFlowGuard({
79
+ * isInternalAuth: true,
80
+ * initLogin: auth.login,
81
+ * });
82
+ *
83
+ * if (!canActivate) {
84
+ * return null; // Login redirect in progress
85
+ * }
86
+ *
87
+ * return children;
88
+ * }
89
+ * ```
90
+ */
91
+ export declare function useAuthenticationFlowGuard(options: AuthenticationFlowGuardOptions): boolean;
92
+ /**
93
+ * Class-based AuthenticationFlowGuard for Angular-like usage patterns
94
+ *
95
+ * This class provides a similar API to Angular's CanActivate guard.
96
+ *
97
+ * @since 3.1.0
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * const guard = new AuthenticationFlowGuard(
102
+ * isInternalAuth,
103
+ * () => auth.login()
104
+ * );
105
+ *
106
+ * if (!guard.canActivate()) {
107
+ * // Navigation blocked, login initiated
108
+ * }
109
+ * ```
110
+ */
111
+ export declare class AuthenticationFlowGuard {
112
+ private isInternalAuth;
113
+ private initLogin;
114
+ constructor(isInternalAuth: boolean, initLogin: () => void | Promise<void>);
115
+ /**
116
+ * Check if navigation should be allowed
117
+ * @returns Whether navigation is allowed
118
+ */
119
+ canActivate(): boolean;
120
+ }
121
+ export default AuthenticationFlowGuard;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Guards
3
+ * Route guards for @abpjs/account
4
+ *
5
+ * @since 3.1.0
6
+ */
7
+ export * from './authentication-flow.guard';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  /**
2
2
  * @abpjs/account
3
3
  * ABP Framework Account module for React
4
- * Translated from @abp/ng.account v3.0.0
4
+ * Translated from @abp/ng.account v3.2.0
5
+ *
6
+ * Changes in v3.2.0:
7
+ * - Breaking: RegisterResponse - Removed twoFactorEnabled property
8
+ * - Dependency update to @abp/ng.theme.shared v3.2.0
9
+ *
10
+ * Changes in v3.1.0:
11
+ * - New: AuthenticationFlowGuard - Route guard for checking auth flow type
12
+ * - ChangePasswordForm: Added hideCurrentPassword prop for users without password (social login)
13
+ * - ManageProfile: Added hideChangePasswordTab prop and loading state for external users
14
+ * - AuthWrapper: Internal refactoring (uses SubscriptionService in Angular, no React impact)
15
+ * - PersonalSettingsForm: Simplified internal implementation
5
16
  *
6
17
  * Changes in v3.0.0:
7
18
  * - New config subpackage: config/providers with ACCOUNT_ROUTE_PROVIDERS, configureRoutes
@@ -31,7 +42,7 @@
31
42
  * - Dependency updates to @abp/ng.theme.shared v2.2.0 and @abp/ng.account.config v2.2.0
32
43
  * - No functional code changes
33
44
  *
34
- * @version 3.0.0
45
+ * @version 3.2.0
35
46
  * @since 2.0.0 - Added Account namespace with component interface types
36
47
  * @since 2.0.0 - Added isSelfRegistrationEnabled support in Login/Register components
37
48
  * @since 2.0.0 - Added enableLocalLogin support in AuthWrapper component
@@ -43,9 +54,12 @@
43
54
  * @since 2.7.0 - Components have static keys for component replacement system
44
55
  * @since 2.9.0 - Version bump only (dependency updates)
45
56
  * @since 3.0.0 - Config subpackage, accountOptionsFactory, initializeAccountRoutes
57
+ * @since 3.1.0 - AuthenticationFlowGuard, hideCurrentPassword, hideChangePasswordTab
58
+ * @since 3.2.0 - RegisterResponse: Removed twoFactorEnabled property
46
59
  */
47
60
  export * from './config';
48
61
  export * from './enums';
62
+ export * from './guards';
49
63
  export * from './models';
50
64
  export * from './services';
51
65
  export * from './utils';
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  AccountProvider: () => AccountProvider,
26
26
  AccountService: () => AccountService,
27
27
  AuthWrapper: () => AuthWrapper,
28
+ AuthenticationFlowGuard: () => AuthenticationFlowGuard,
28
29
  ChangePasswordForm: () => ChangePasswordForm,
29
30
  DEFAULT_REDIRECT_URL: () => DEFAULT_REDIRECT_URL,
30
31
  LoginForm: () => LoginForm,
@@ -35,6 +36,7 @@ __export(index_exports, {
35
36
  RegisterPage: () => RegisterPage,
36
37
  TenantBox: () => TenantBox,
37
38
  accountOptionsFactory: () => accountOptionsFactory,
39
+ authenticationFlowGuard: () => authenticationFlowGuard,
38
40
  configureRoutes: () => configureRoutes,
39
41
  eAccountComponents: () => eAccountComponents,
40
42
  eAccountRouteNames: () => eAccountRouteNames,
@@ -42,6 +44,7 @@ __export(index_exports, {
42
44
  useAccountContext: () => useAccountContext,
43
45
  useAccountOptions: () => useAccountOptions,
44
46
  useAccountService: () => useAccountService,
47
+ useAuthenticationFlowGuard: () => useAuthenticationFlowGuard,
45
48
  usePasswordFlow: () => usePasswordFlow,
46
49
  useSelfRegistrationEnabled: () => useSelfRegistrationEnabled
47
50
  });
@@ -144,6 +147,40 @@ var eAccountComponents = {
144
147
  PersonalSettings: "Account.PersonalSettingsComponent"
145
148
  };
146
149
 
150
+ // src/guards/authentication-flow.guard.ts
151
+ function authenticationFlowGuard(options) {
152
+ const { isInternalAuth, initLogin } = options;
153
+ if (isInternalAuth) {
154
+ return { canActivate: true };
155
+ }
156
+ initLogin();
157
+ return {
158
+ canActivate: false,
159
+ reason: "external_auth"
160
+ };
161
+ }
162
+ function useAuthenticationFlowGuard(options) {
163
+ const result = authenticationFlowGuard(options);
164
+ return result.canActivate;
165
+ }
166
+ var AuthenticationFlowGuard = class {
167
+ constructor(isInternalAuth, initLogin) {
168
+ this.isInternalAuth = isInternalAuth;
169
+ this.initLogin = initLogin;
170
+ }
171
+ /**
172
+ * Check if navigation should be allowed
173
+ * @returns Whether navigation is allowed
174
+ */
175
+ canActivate() {
176
+ if (this.isInternalAuth) {
177
+ return true;
178
+ }
179
+ this.initLogin();
180
+ return false;
181
+ }
182
+ };
183
+
147
184
  // src/services/account.service.ts
148
185
  var AccountService = class {
149
186
  constructor(rest) {
@@ -389,31 +426,58 @@ var passwordValidation = {
389
426
  hasNumber: /[0-9]/,
390
427
  hasSpecial: /[!@#$%^&*(),.?":{}|<>]/
391
428
  };
392
- var changePasswordSchema = import_zod2.z.object({
393
- currentPassword: import_zod2.z.string().min(1, "Current password is required"),
394
- newPassword: import_zod2.z.string().min(6, "Password must be at least 6 characters").max(32, "Password must be at most 32 characters").refine(
395
- (val) => passwordValidation.hasLowercase.test(val),
396
- "Password must contain at least one lowercase letter"
397
- ).refine(
398
- (val) => passwordValidation.hasUppercase.test(val),
399
- "Password must contain at least one uppercase letter"
400
- ).refine(
401
- (val) => passwordValidation.hasNumber.test(val),
402
- "Password must contain at least one number"
403
- ).refine(
404
- (val) => passwordValidation.hasSpecial.test(val),
405
- "Password must contain at least one special character"
406
- ),
407
- confirmNewPassword: import_zod2.z.string().min(1, "Confirm password is required")
408
- }).refine((data) => data.newPassword === data.confirmNewPassword, {
409
- message: "Passwords do not match",
410
- path: ["confirmNewPassword"]
411
- });
412
- function ChangePasswordForm({ onSuccess, onError }) {
429
+ function createChangePasswordSchema(hideCurrentPassword) {
430
+ const baseSchema = {
431
+ newPassword: import_zod2.z.string().min(6, "Password must be at least 6 characters").max(32, "Password must be at most 32 characters").refine(
432
+ (val) => passwordValidation.hasLowercase.test(val),
433
+ "Password must contain at least one lowercase letter"
434
+ ).refine(
435
+ (val) => passwordValidation.hasUppercase.test(val),
436
+ "Password must contain at least one uppercase letter"
437
+ ).refine(
438
+ (val) => passwordValidation.hasNumber.test(val),
439
+ "Password must contain at least one number"
440
+ ).refine(
441
+ (val) => passwordValidation.hasSpecial.test(val),
442
+ "Password must contain at least one special character"
443
+ ),
444
+ confirmNewPassword: import_zod2.z.string().min(1, "Confirm password is required")
445
+ };
446
+ const schema = hideCurrentPassword ? import_zod2.z.object({
447
+ currentPassword: import_zod2.z.string().optional(),
448
+ ...baseSchema
449
+ }) : import_zod2.z.object({
450
+ currentPassword: import_zod2.z.string().min(1, "Current password is required"),
451
+ ...baseSchema
452
+ });
453
+ return schema.refine((data) => data.newPassword === data.confirmNewPassword, {
454
+ message: "Passwords do not match",
455
+ path: ["confirmNewPassword"]
456
+ });
457
+ }
458
+ function ChangePasswordForm({
459
+ onSuccess,
460
+ onError,
461
+ hideCurrentPassword: hideCurrentPasswordProp
462
+ }) {
413
463
  const { t } = (0, import_core6.useLocalization)();
414
- const { changePassword } = (0, import_core6.useProfile)();
464
+ const { profile, changePassword } = (0, import_core6.useProfile)();
415
465
  const toaster = (0, import_theme_shared.useToaster)();
416
466
  const [inProgress, setInProgress] = (0, import_react5.useState)(false);
467
+ const [showCurrentPasswordAfterChange, setShowCurrentPasswordAfterChange] = (0, import_react5.useState)(false);
468
+ const shouldHideCurrentPassword = (0, import_react5.useMemo)(() => {
469
+ if (hideCurrentPasswordProp !== void 0) {
470
+ return hideCurrentPasswordProp;
471
+ }
472
+ if (showCurrentPasswordAfterChange) {
473
+ return false;
474
+ }
475
+ return profile?.hasPassword === false;
476
+ }, [hideCurrentPasswordProp, profile?.hasPassword, showCurrentPasswordAfterChange]);
477
+ const changePasswordSchema = (0, import_react5.useMemo)(
478
+ () => createChangePasswordSchema(shouldHideCurrentPassword),
479
+ [shouldHideCurrentPassword]
480
+ );
417
481
  const {
418
482
  register,
419
483
  handleSubmit,
@@ -427,13 +491,13 @@ function ChangePasswordForm({ onSuccess, onError }) {
427
491
  confirmNewPassword: ""
428
492
  }
429
493
  });
430
- (0, import_react5.useEffect)(() => {
431
- }, []);
432
494
  const onSubmit = async (data) => {
433
495
  setInProgress(true);
434
496
  try {
435
497
  await changePassword({
436
- currentPassword: data.currentPassword,
498
+ // Only include currentPassword if not hidden
499
+ // v3.1.0: Support for users without password (social login)
500
+ ...!shouldHideCurrentPassword && data.currentPassword ? { currentPassword: data.currentPassword } : {},
437
501
  newPassword: data.newPassword
438
502
  });
439
503
  toaster.success(
@@ -441,6 +505,9 @@ function ChangePasswordForm({ onSuccess, onError }) {
441
505
  t("AbpAccount::Success") || "Success"
442
506
  );
443
507
  reset();
508
+ if (shouldHideCurrentPassword) {
509
+ setShowCurrentPasswordAfterChange(true);
510
+ }
444
511
  onSuccess?.();
445
512
  } catch (err) {
446
513
  const errorMessage = err?.error?.error_description || err?.error?.error?.message || t("AbpAccount::DefaultErrorMessage") || "An error occurred";
@@ -451,7 +518,7 @@ function ChangePasswordForm({ onSuccess, onError }) {
451
518
  }
452
519
  };
453
520
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react6.Stack, { gap: "5", children: [
454
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react7.Field.Root, { invalid: !!errors.currentPassword, children: [
521
+ !shouldHideCurrentPassword && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_react7.Field.Root, { invalid: !!errors.currentPassword, children: [
455
522
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react7.Field.Label, { children: t("AbpAccount::CurrentPassword") }),
456
523
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react7.InputGroup, { startElement: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lu.LuLock, {}), width: "full", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
457
524
  import_react6.Input,
@@ -950,11 +1017,26 @@ var import_jsx_runtime7 = require("react/jsx-runtime");
950
1017
  function ManageProfile({
951
1018
  defaultTabIndex = 0,
952
1019
  onTabChange,
953
- customTabs
1020
+ customTabs,
1021
+ hideChangePasswordTab: hideChangePasswordTabProp
954
1022
  }) {
955
1023
  const { t } = (0, import_core10.useLocalization)();
1024
+ const { profile, loading: profileLoading, fetchProfile } = (0, import_core10.useProfile)();
1025
+ const [isProfileLoaded, setIsProfileLoaded] = (0, import_react15.useState)(false);
956
1026
  const [selectedTab, setSelectedTab] = (0, import_react15.useState)(defaultTabIndex);
957
- const defaultTabs = [
1027
+ (0, import_react15.useEffect)(() => {
1028
+ fetchProfile().then(() => {
1029
+ setIsProfileLoaded(true);
1030
+ });
1031
+ }, [fetchProfile]);
1032
+ const shouldHideChangePasswordTab = hideChangePasswordTabProp ?? profile?.isExternal ?? false;
1033
+ (0, import_react15.useEffect)(() => {
1034
+ if (isProfileLoaded && shouldHideChangePasswordTab && selectedTab === 0) {
1035
+ const personalSettingsIndex = 0;
1036
+ setSelectedTab(personalSettingsIndex);
1037
+ }
1038
+ }, [isProfileLoaded, shouldHideChangePasswordTab, selectedTab]);
1039
+ const allTabs = [
958
1040
  {
959
1041
  id: "personal-settings",
960
1042
  label: t("AbpAccount::PersonalSettings") || "Personal Settings",
@@ -963,9 +1045,11 @@ function ManageProfile({
963
1045
  {
964
1046
  id: "change-password",
965
1047
  label: t("AbpAccount::ChangePassword") || "Change Password",
966
- content: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChangePasswordForm, {})
1048
+ // v3.1.0: Pass hideCurrentPassword based on profile.hasPassword
1049
+ content: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChangePasswordForm, { hideCurrentPassword: profile?.hasPassword === false })
967
1050
  }
968
1051
  ];
1052
+ const defaultTabs = shouldHideChangePasswordTab ? allTabs.filter((tab) => tab.id !== "change-password") : allTabs;
969
1053
  const tabs = customTabs || defaultTabs;
970
1054
  const handleTabChange = (details) => {
971
1055
  const index = tabs.findIndex((tab) => tab.id === details.value);
@@ -974,6 +1058,9 @@ function ManageProfile({
974
1058
  onTabChange?.(index);
975
1059
  }
976
1060
  };
1061
+ if (!isProfileLoaded || profileLoading) {
1062
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Box, { className: "manage-profile", py: { base: "8", md: "12" }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Container, { maxW: "2xl", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Center, { minH: "400px", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Spinner, { size: "xl" }) }) }) });
1063
+ }
977
1064
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Box, { className: "manage-profile", py: { base: "8", md: "12" }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Container, { maxW: "2xl", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_react16.Stack, { gap: "8", children: [
978
1065
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react16.Heading, { size: "xl", children: t("AbpAccount::ManageYourAccount") || "Manage Your Account" }),
979
1066
  /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
@@ -1206,6 +1293,7 @@ var ACCOUNT_PATHS = {
1206
1293
  AccountProvider,
1207
1294
  AccountService,
1208
1295
  AuthWrapper,
1296
+ AuthenticationFlowGuard,
1209
1297
  ChangePasswordForm,
1210
1298
  DEFAULT_REDIRECT_URL,
1211
1299
  LoginForm,
@@ -1216,6 +1304,7 @@ var ACCOUNT_PATHS = {
1216
1304
  RegisterPage,
1217
1305
  TenantBox,
1218
1306
  accountOptionsFactory,
1307
+ authenticationFlowGuard,
1219
1308
  configureRoutes,
1220
1309
  eAccountComponents,
1221
1310
  eAccountRouteNames,
@@ -1223,6 +1312,7 @@ var ACCOUNT_PATHS = {
1223
1312
  useAccountContext,
1224
1313
  useAccountOptions,
1225
1314
  useAccountService,
1315
+ useAuthenticationFlowGuard,
1226
1316
  usePasswordFlow,
1227
1317
  useSelfRegistrationEnabled
1228
1318
  });
package/dist/index.mjs CHANGED
@@ -95,6 +95,40 @@ var eAccountComponents = {
95
95
  PersonalSettings: "Account.PersonalSettingsComponent"
96
96
  };
97
97
 
98
+ // src/guards/authentication-flow.guard.ts
99
+ function authenticationFlowGuard(options) {
100
+ const { isInternalAuth, initLogin } = options;
101
+ if (isInternalAuth) {
102
+ return { canActivate: true };
103
+ }
104
+ initLogin();
105
+ return {
106
+ canActivate: false,
107
+ reason: "external_auth"
108
+ };
109
+ }
110
+ function useAuthenticationFlowGuard(options) {
111
+ const result = authenticationFlowGuard(options);
112
+ return result.canActivate;
113
+ }
114
+ var AuthenticationFlowGuard = class {
115
+ constructor(isInternalAuth, initLogin) {
116
+ this.isInternalAuth = isInternalAuth;
117
+ this.initLogin = initLogin;
118
+ }
119
+ /**
120
+ * Check if navigation should be allowed
121
+ * @returns Whether navigation is allowed
122
+ */
123
+ canActivate() {
124
+ if (this.isInternalAuth) {
125
+ return true;
126
+ }
127
+ this.initLogin();
128
+ return false;
129
+ }
130
+ };
131
+
98
132
  // src/services/account.service.ts
99
133
  var AccountService = class {
100
134
  constructor(rest) {
@@ -324,7 +358,7 @@ function AuthWrapper({
324
358
  AuthWrapper.tenantBoxKey = eAccountComponents.TenantBox;
325
359
 
326
360
  // src/components/ChangePasswordForm/ChangePasswordForm.tsx
327
- import { useState as useState2, useEffect } from "react";
361
+ import { useState as useState2, useMemo as useMemo3 } from "react";
328
362
  import { useForm } from "react-hook-form";
329
363
  import { zodResolver } from "@hookform/resolvers/zod";
330
364
  import { z } from "zod";
@@ -340,31 +374,58 @@ var passwordValidation = {
340
374
  hasNumber: /[0-9]/,
341
375
  hasSpecial: /[!@#$%^&*(),.?":{}|<>]/
342
376
  };
343
- var changePasswordSchema = z.object({
344
- currentPassword: z.string().min(1, "Current password is required"),
345
- newPassword: z.string().min(6, "Password must be at least 6 characters").max(32, "Password must be at most 32 characters").refine(
346
- (val) => passwordValidation.hasLowercase.test(val),
347
- "Password must contain at least one lowercase letter"
348
- ).refine(
349
- (val) => passwordValidation.hasUppercase.test(val),
350
- "Password must contain at least one uppercase letter"
351
- ).refine(
352
- (val) => passwordValidation.hasNumber.test(val),
353
- "Password must contain at least one number"
354
- ).refine(
355
- (val) => passwordValidation.hasSpecial.test(val),
356
- "Password must contain at least one special character"
357
- ),
358
- confirmNewPassword: z.string().min(1, "Confirm password is required")
359
- }).refine((data) => data.newPassword === data.confirmNewPassword, {
360
- message: "Passwords do not match",
361
- path: ["confirmNewPassword"]
362
- });
363
- function ChangePasswordForm({ onSuccess, onError }) {
377
+ function createChangePasswordSchema(hideCurrentPassword) {
378
+ const baseSchema = {
379
+ newPassword: z.string().min(6, "Password must be at least 6 characters").max(32, "Password must be at most 32 characters").refine(
380
+ (val) => passwordValidation.hasLowercase.test(val),
381
+ "Password must contain at least one lowercase letter"
382
+ ).refine(
383
+ (val) => passwordValidation.hasUppercase.test(val),
384
+ "Password must contain at least one uppercase letter"
385
+ ).refine(
386
+ (val) => passwordValidation.hasNumber.test(val),
387
+ "Password must contain at least one number"
388
+ ).refine(
389
+ (val) => passwordValidation.hasSpecial.test(val),
390
+ "Password must contain at least one special character"
391
+ ),
392
+ confirmNewPassword: z.string().min(1, "Confirm password is required")
393
+ };
394
+ const schema = hideCurrentPassword ? z.object({
395
+ currentPassword: z.string().optional(),
396
+ ...baseSchema
397
+ }) : z.object({
398
+ currentPassword: z.string().min(1, "Current password is required"),
399
+ ...baseSchema
400
+ });
401
+ return schema.refine((data) => data.newPassword === data.confirmNewPassword, {
402
+ message: "Passwords do not match",
403
+ path: ["confirmNewPassword"]
404
+ });
405
+ }
406
+ function ChangePasswordForm({
407
+ onSuccess,
408
+ onError,
409
+ hideCurrentPassword: hideCurrentPasswordProp
410
+ }) {
364
411
  const { t } = useLocalization2();
365
- const { changePassword } = useProfile();
412
+ const { profile, changePassword } = useProfile();
366
413
  const toaster = useToaster();
367
414
  const [inProgress, setInProgress] = useState2(false);
415
+ const [showCurrentPasswordAfterChange, setShowCurrentPasswordAfterChange] = useState2(false);
416
+ const shouldHideCurrentPassword = useMemo3(() => {
417
+ if (hideCurrentPasswordProp !== void 0) {
418
+ return hideCurrentPasswordProp;
419
+ }
420
+ if (showCurrentPasswordAfterChange) {
421
+ return false;
422
+ }
423
+ return profile?.hasPassword === false;
424
+ }, [hideCurrentPasswordProp, profile?.hasPassword, showCurrentPasswordAfterChange]);
425
+ const changePasswordSchema = useMemo3(
426
+ () => createChangePasswordSchema(shouldHideCurrentPassword),
427
+ [shouldHideCurrentPassword]
428
+ );
368
429
  const {
369
430
  register,
370
431
  handleSubmit,
@@ -378,13 +439,13 @@ function ChangePasswordForm({ onSuccess, onError }) {
378
439
  confirmNewPassword: ""
379
440
  }
380
441
  });
381
- useEffect(() => {
382
- }, []);
383
442
  const onSubmit = async (data) => {
384
443
  setInProgress(true);
385
444
  try {
386
445
  await changePassword({
387
- currentPassword: data.currentPassword,
446
+ // Only include currentPassword if not hidden
447
+ // v3.1.0: Support for users without password (social login)
448
+ ...!shouldHideCurrentPassword && data.currentPassword ? { currentPassword: data.currentPassword } : {},
388
449
  newPassword: data.newPassword
389
450
  });
390
451
  toaster.success(
@@ -392,6 +453,9 @@ function ChangePasswordForm({ onSuccess, onError }) {
392
453
  t("AbpAccount::Success") || "Success"
393
454
  );
394
455
  reset();
456
+ if (shouldHideCurrentPassword) {
457
+ setShowCurrentPasswordAfterChange(true);
458
+ }
395
459
  onSuccess?.();
396
460
  } catch (err) {
397
461
  const errorMessage = err?.error?.error_description || err?.error?.error?.message || t("AbpAccount::DefaultErrorMessage") || "An error occurred";
@@ -402,7 +466,7 @@ function ChangePasswordForm({ onSuccess, onError }) {
402
466
  }
403
467
  };
404
468
  return /* @__PURE__ */ jsx3("form", { onSubmit: handleSubmit(onSubmit), noValidate: true, children: /* @__PURE__ */ jsxs2(Stack2, { gap: "5", children: [
405
- /* @__PURE__ */ jsxs2(Field.Root, { invalid: !!errors.currentPassword, children: [
469
+ !shouldHideCurrentPassword && /* @__PURE__ */ jsxs2(Field.Root, { invalid: !!errors.currentPassword, children: [
406
470
  /* @__PURE__ */ jsx3(Field.Label, { children: t("AbpAccount::CurrentPassword") }),
407
471
  /* @__PURE__ */ jsx3(InputGroup, { startElement: /* @__PURE__ */ jsx3(LuLock, {}), width: "full", children: /* @__PURE__ */ jsx3(
408
472
  Input,
@@ -467,7 +531,7 @@ import { Alert, Button as Button3, Checkbox } from "@abpjs/theme-shared";
467
531
  import { Box as Box3, Heading, Input as Input3, Link as Link2, HStack, Show } from "@chakra-ui/react";
468
532
 
469
533
  // src/components/TenantBox/TenantBox.tsx
470
- import { useState as useState3, useCallback as useCallback2, useEffect as useEffect2 } from "react";
534
+ import { useState as useState3, useCallback as useCallback2, useEffect } from "react";
471
535
  import { useDispatch, useSelector } from "react-redux";
472
536
  import { useLocalization as useLocalization3, sessionActions, selectTenant } from "@abpjs/core";
473
537
  import { Modal, Button as Button2, useToaster as useToaster2 } from "@abpjs/theme-shared";
@@ -482,7 +546,7 @@ function TenantBox({ containerStyle }) {
482
546
  const [name, setName] = useState3("");
483
547
  const [isModalVisible, setIsModalVisible] = useState3(false);
484
548
  const [modalBusy, setModalBusy] = useState3(false);
485
- useEffect2(() => {
549
+ useEffect(() => {
486
550
  setName(currentTenant?.name || "");
487
551
  }, [currentTenant]);
488
552
  const onSwitch = useCallback2(() => {
@@ -742,12 +806,12 @@ function LoginForm({
742
806
  LoginForm.authWrapperKey = eAccountComponents.AuthWrapper;
743
807
 
744
808
  // src/components/ManageProfile/ManageProfile.tsx
745
- import { useState as useState5 } from "react";
746
- import { useLocalization as useLocalization6 } from "@abpjs/core";
747
- import { Box as Box4, Container as Container3, Heading as Heading2, Stack as Stack5, Tabs } from "@chakra-ui/react";
809
+ import { useState as useState5, useEffect as useEffect3 } from "react";
810
+ import { useLocalization as useLocalization6, useProfile as useProfile3 } from "@abpjs/core";
811
+ import { Box as Box4, Container as Container3, Heading as Heading2, Stack as Stack5, Tabs, Spinner, Center } from "@chakra-ui/react";
748
812
 
749
813
  // src/components/PersonalSettingsForm/PersonalSettingsForm.tsx
750
- import { useState as useState4, useEffect as useEffect3 } from "react";
814
+ import { useState as useState4, useEffect as useEffect2 } from "react";
751
815
  import { useForm as useForm3 } from "react-hook-form";
752
816
  import { zodResolver as zodResolver3 } from "@hookform/resolvers/zod";
753
817
  import { z as z3 } from "zod";
@@ -784,10 +848,10 @@ function PersonalSettingsForm({ onSuccess, onError }) {
784
848
  phoneNumber: ""
785
849
  }
786
850
  });
787
- useEffect3(() => {
851
+ useEffect2(() => {
788
852
  fetchProfile();
789
853
  }, [fetchProfile]);
790
- useEffect3(() => {
854
+ useEffect2(() => {
791
855
  if (profile) {
792
856
  reset({
793
857
  userName: profile.userName || "",
@@ -909,11 +973,26 @@ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
909
973
  function ManageProfile({
910
974
  defaultTabIndex = 0,
911
975
  onTabChange,
912
- customTabs
976
+ customTabs,
977
+ hideChangePasswordTab: hideChangePasswordTabProp
913
978
  }) {
914
979
  const { t } = useLocalization6();
980
+ const { profile, loading: profileLoading, fetchProfile } = useProfile3();
981
+ const [isProfileLoaded, setIsProfileLoaded] = useState5(false);
915
982
  const [selectedTab, setSelectedTab] = useState5(defaultTabIndex);
916
- const defaultTabs = [
983
+ useEffect3(() => {
984
+ fetchProfile().then(() => {
985
+ setIsProfileLoaded(true);
986
+ });
987
+ }, [fetchProfile]);
988
+ const shouldHideChangePasswordTab = hideChangePasswordTabProp ?? profile?.isExternal ?? false;
989
+ useEffect3(() => {
990
+ if (isProfileLoaded && shouldHideChangePasswordTab && selectedTab === 0) {
991
+ const personalSettingsIndex = 0;
992
+ setSelectedTab(personalSettingsIndex);
993
+ }
994
+ }, [isProfileLoaded, shouldHideChangePasswordTab, selectedTab]);
995
+ const allTabs = [
917
996
  {
918
997
  id: "personal-settings",
919
998
  label: t("AbpAccount::PersonalSettings") || "Personal Settings",
@@ -922,9 +1001,11 @@ function ManageProfile({
922
1001
  {
923
1002
  id: "change-password",
924
1003
  label: t("AbpAccount::ChangePassword") || "Change Password",
925
- content: /* @__PURE__ */ jsx7(ChangePasswordForm, {})
1004
+ // v3.1.0: Pass hideCurrentPassword based on profile.hasPassword
1005
+ content: /* @__PURE__ */ jsx7(ChangePasswordForm, { hideCurrentPassword: profile?.hasPassword === false })
926
1006
  }
927
1007
  ];
1008
+ const defaultTabs = shouldHideChangePasswordTab ? allTabs.filter((tab) => tab.id !== "change-password") : allTabs;
928
1009
  const tabs = customTabs || defaultTabs;
929
1010
  const handleTabChange = (details) => {
930
1011
  const index = tabs.findIndex((tab) => tab.id === details.value);
@@ -933,6 +1014,9 @@ function ManageProfile({
933
1014
  onTabChange?.(index);
934
1015
  }
935
1016
  };
1017
+ if (!isProfileLoaded || profileLoading) {
1018
+ return /* @__PURE__ */ jsx7(Box4, { className: "manage-profile", py: { base: "8", md: "12" }, children: /* @__PURE__ */ jsx7(Container3, { maxW: "2xl", children: /* @__PURE__ */ jsx7(Center, { minH: "400px", children: /* @__PURE__ */ jsx7(Spinner, { size: "xl" }) }) }) });
1019
+ }
936
1020
  return /* @__PURE__ */ jsx7(Box4, { className: "manage-profile", py: { base: "8", md: "12" }, children: /* @__PURE__ */ jsx7(Container3, { maxW: "2xl", children: /* @__PURE__ */ jsxs6(Stack5, { gap: "8", children: [
937
1021
  /* @__PURE__ */ jsx7(Heading2, { size: "xl", children: t("AbpAccount::ManageYourAccount") || "Manage Your Account" }),
938
1022
  /* @__PURE__ */ jsxs6(
@@ -1141,23 +1225,23 @@ function RegisterForm({
1141
1225
  RegisterForm.authWrapperKey = eAccountComponents.AuthWrapper;
1142
1226
 
1143
1227
  // src/pages/LoginPage.tsx
1144
- import { Container as Container5, Center } from "@chakra-ui/react";
1228
+ import { Container as Container5, Center as Center2 } from "@chakra-ui/react";
1145
1229
  import { jsx as jsx9 } from "react/jsx-runtime";
1146
1230
  function LoginPage({
1147
1231
  maxWidth = "container.sm",
1148
1232
  ...loginFormProps
1149
1233
  }) {
1150
- return /* @__PURE__ */ jsx9(Container5, { maxW: maxWidth, py: 10, children: /* @__PURE__ */ jsx9(Center, { children: /* @__PURE__ */ jsx9(LoginForm, { ...loginFormProps }) }) });
1234
+ return /* @__PURE__ */ jsx9(Container5, { maxW: maxWidth, py: 10, children: /* @__PURE__ */ jsx9(Center2, { children: /* @__PURE__ */ jsx9(LoginForm, { ...loginFormProps }) }) });
1151
1235
  }
1152
1236
 
1153
1237
  // src/pages/RegisterPage.tsx
1154
- import { Container as Container6, Center as Center2 } from "@chakra-ui/react";
1238
+ import { Container as Container6, Center as Center3 } from "@chakra-ui/react";
1155
1239
  import { jsx as jsx10 } from "react/jsx-runtime";
1156
1240
  function RegisterPage({
1157
1241
  maxWidth = "container.sm",
1158
1242
  ...registerFormProps
1159
1243
  }) {
1160
- return /* @__PURE__ */ jsx10(Container6, { maxW: maxWidth, py: 10, children: /* @__PURE__ */ jsx10(Center2, { children: /* @__PURE__ */ jsx10(RegisterForm, { ...registerFormProps }) }) });
1244
+ return /* @__PURE__ */ jsx10(Container6, { maxW: maxWidth, py: 10, children: /* @__PURE__ */ jsx10(Center3, { children: /* @__PURE__ */ jsx10(RegisterForm, { ...registerFormProps }) }) });
1161
1245
  }
1162
1246
 
1163
1247
  // src/routes/index.ts
@@ -1172,6 +1256,7 @@ export {
1172
1256
  AccountProvider,
1173
1257
  AccountService,
1174
1258
  AuthWrapper,
1259
+ AuthenticationFlowGuard,
1175
1260
  ChangePasswordForm,
1176
1261
  DEFAULT_REDIRECT_URL,
1177
1262
  LoginForm,
@@ -1182,6 +1267,7 @@ export {
1182
1267
  RegisterPage,
1183
1268
  TenantBox,
1184
1269
  accountOptionsFactory,
1270
+ authenticationFlowGuard,
1185
1271
  configureRoutes,
1186
1272
  eAccountComponents,
1187
1273
  eAccountRouteNames,
@@ -1189,6 +1275,7 @@ export {
1189
1275
  useAccountContext,
1190
1276
  useAccountOptions,
1191
1277
  useAccountService,
1278
+ useAuthenticationFlowGuard,
1192
1279
  usePasswordFlow,
1193
1280
  useSelfRegistrationEnabled
1194
1281
  };
@@ -100,6 +100,8 @@ export interface RegisterRequest {
100
100
  }
101
101
  /**
102
102
  * Response from user registration API
103
+ *
104
+ * @since 3.2.0 - Removed twoFactorEnabled property
103
105
  */
104
106
  export interface RegisterResponse {
105
107
  tenantId: string;
@@ -110,7 +112,6 @@ export interface RegisterResponse {
110
112
  emailConfirmed: boolean;
111
113
  phoneNumber: string;
112
114
  phoneNumberConfirmed: boolean;
113
- twoFactorEnabled: boolean;
114
115
  lockoutEnabled: boolean;
115
116
  lockoutEnd: string;
116
117
  concurrencyStamp: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abpjs/account",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "ABP Framework Account module for React - Translation of @abp/ng.account",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -53,11 +53,11 @@
53
53
  "react-redux": "^9.0.0",
54
54
  "zod": "^3.22.0",
55
55
  "react-icons": "^5.0.0",
56
- "@abpjs/core": "3.0.0",
57
- "@abpjs/theme-shared": "3.0.0"
56
+ "@abpjs/theme-shared": "3.2.0",
57
+ "@abpjs/core": "3.2.0"
58
58
  },
59
59
  "devDependencies": {
60
- "@abp/ng.account": "3.0.0",
60
+ "@abp/ng.account": "3.2.0",
61
61
  "@reduxjs/toolkit": "^2.0.0",
62
62
  "@testing-library/jest-dom": "^6.4.0",
63
63
  "@testing-library/react": "^14.2.0",