@@ -4,11 +4,11 @@ import Modal from 'react-bootstrap/Modal';
44import { Tabs , Tab } from 'react-bootstrap' ;
55import { CloseIcon , V8CustomButton , CustomInfo , SelectDropdown , CustomTextInput , ApplicationLogo , PromptModal } from "@formsflow/components" ;
66import { fetchSelectLanguages } from '../services/language' ;
7- import { requestResetPassword } from " ../services/user" ;
7+ import { updateUserProfile , requestResetPassword } from ' ../services/user' ;
88import { useTranslation } from "react-i18next" ;
99import i18n from '../resourceBundles/i18n' ;
1010import { StorageService } from "@formsflow/service" ;
11- import { KEYCLOAK_AUTH_URL , KEYCLOAK_REALM , LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
11+ import { LANGUAGE , MULTITENANCY_ENABLED , USER_LANGUAGE_LIST } from '../constants/constants' ;
1212import { fetchPermissions } from '../services/permissions' ;
1313import { getUserPermissionsByCategory } from '../helper/helper' ;
1414
@@ -17,7 +17,7 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
1717 const prevSelectedLang = localStorage . getItem ( 'i18nextLng' ) ;
1818 const [ selectedLang , setSelectedLang ] = useState ( prevSelectedLang || LANGUAGE ) ;
1919 const [ activeTab , setActiveTab ] = useState ( "Profile" ) ;
20- const isSSO = false ;
20+ const [ isSSO , setIsSSO ] = useState ( false ) ;
2121 const [ showUnsavedChangesPrompt , setShowUnsavedChangesPrompt ] = useState ( false ) ;
2222 const [ profileFields , setProfileFields ] = useState ( {
2323 firstName : "" ,
@@ -33,10 +33,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
3333 const [ resetPasswordState , setResetPasswordState ] = useState ( "default" ) ; // default | success | error
3434 const [ resetPasswordLoading , setResetPasswordLoading ] = useState ( false ) ;
3535 const [ lastResetPasswordError , setLastResetPasswordError ] = useState ( null ) ;
36+ const [ userId , setUserId ] = useState ( "" ) ;
37+ const [ isLoading , setIsLoading ] = useState ( false ) ;
38+ const [ error , setError ] = useState ( null ) ;
3639 const { t } = useTranslation ( ) ;
3740
3841 useEffect ( ( ) => {
3942 if ( ! show ) return ;
43+ setError ( null ) ;
4044 try {
4145 // Reset language selection to current app language when modal opens
4246 const currentLang = localStorage . getItem ( "i18nextLng" ) || LANGUAGE ;
@@ -48,6 +52,26 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
4852 const [ firstFromName = "" , ...rest ] = String ( fullName ) . trim ( ) . split ( / \s + / ) ;
4953 const lastFromName = rest . join ( " " ) ;
5054
55+ // Check if user is logged in via federated identity provider from USER_LOGIN_DETAILS
56+ // loginType can be "internal" (Keycloak) or "external" (external IDP like Google/Microsoft)
57+ // If loginType is "external", disable profile editing fields
58+ let federatedLogin = false ;
59+ try {
60+ const userLoginDetailsStr = localStorage . getItem ( "USER_LOGIN_DETAILS" ) ;
61+ if ( userLoginDetailsStr ) {
62+ const userLoginDetails = JSON . parse ( userLoginDetailsStr ) ;
63+ // Check loginType: "external" means SSO/external IDP, "internal" means Keycloak
64+ if ( userLoginDetails ?. loginType !== undefined ) {
65+ const loginType = String ( userLoginDetails . loginType ) . trim ( ) . toLowerCase ( ) ;
66+ federatedLogin = loginType === "external" ;
67+ }
68+ }
69+ } catch ( e ) {
70+ // Fallback to checking userDetail if USER_LOGIN_DETAILS is not available
71+ federatedLogin = ! ! ( userDetail ?. identityProvider || userDetail ?. identity_provider ) ;
72+ }
73+ setIsSSO ( federatedLogin ) ;
74+
5175 const nextFields = {
5276 firstName : userDetail ?. given_name || firstFromName || "" ,
5377 lastName : userDetail ?. family_name || lastFromName || "" ,
@@ -61,11 +85,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
6185 setResetPasswordState ( "default" ) ;
6286 setResetPasswordLoading ( false ) ;
6387 setLastResetPasswordError ( null ) ;
88+ setUserId ( userDetail ?. sub || userDetail ?. id || "" ) ;
6489 } catch ( e ) {
6590 setProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
6691 setInitialProfileFields ( { firstName : "" , lastName : "" , email : "" , username : "" } ) ;
6792 setSelectedLang ( prevSelectedLang || LANGUAGE ) ;
6893 setInitialSelectedLang ( prevSelectedLang || LANGUAGE ) ;
94+ setUserId ( "" ) ;
95+ setIsSSO ( false ) ;
6996 }
7097 } , [ show ] ) ;
7198
@@ -78,29 +105,6 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
78105 const supportedLanguages = languages . filter ( item => userLanguagesArray . includes ( item . name ) ) ;
79106 setSelectLanguages ( supportedLanguages . length > 0 ? supportedLanguages : languages ) ;
80107 } ) ;
81-
82- // Calculate remaining days from expiry_dt
83- try {
84- const tenantDataStr = StorageService . get ( "tenantData" ) ;
85- const expiry_dt = tenantDataStr
86- ? JSON . parse ( tenantDataStr ) ?. expiry_dt
87- : tenant ?. tenantData ?. expiry_dt ;
88-
89- if ( expiry_dt && ! Number . isNaN ( Date . parse ( expiry_dt ) ) ) {
90- const expiry = new Date ( expiry_dt ) ;
91- const currentDate = new Date ( ) ;
92- currentDate . setHours ( 0 , 0 , 0 , 0 ) ;
93- expiry . setHours ( 0 , 0 , 0 , 0 ) ;
94- const timeDifference = expiry . getTime ( ) - currentDate . getTime ( ) ;
95- const days = Math . floor ( timeDifference / ( 1000 * 60 * 60 * 24 ) ) ;
96- setDaysDifference ( days ) ;
97- } else {
98- setDaysDifference ( null ) ;
99- }
100- } catch ( error ) {
101- console . error ( "Error calculating days difference:" , error ) ;
102- setDaysDifference ( null ) ;
103- }
104108 } , [ ] ) ;
105109
106110 // Fetch user permissions when modal opens
@@ -177,37 +181,128 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
177181 StorageService . save ( "PROFILE_RESET_PASSWORD_LAST_ERROR" , JSON . stringify ( details ) ) ;
178182 } catch ( _ ) {
179183 }
180-
184+
181185 console . error ( "Reset password failed:" , details ) ;
182186 } finally {
183187 setResetPasswordLoading ( false ) ;
184188 }
185189 } ;
186190
187- const handleConfirmProfile = ( ) => {
188- // Keep a copy for later integration; for now just save it locally and close the modal.
189- const firstName = ( profileFields . firstName || "" ) . trim ( ) ;
190- const lastName = ( profileFields . lastName || "" ) . trim ( ) ;
191-
192- const userCopy = {
193- user : {
194- firstName,
195- lastName,
196- userName : ( profileFields . username || "" ) . trim ( ) ,
197- email : ( profileFields . email || "" ) . trim ( ) ,
198- attributes : {
199- locale : [ selectedLang ] ,
200- } ,
201- } ,
191+ // Helper: Build profile payload with only changed fields
192+ const buildProfilePayload = ( currentFields , initialFields , currentLang , prevLang ) => {
193+ const payload = { } ;
194+ const trimmedFields = {
195+ firstName : ( currentFields . firstName || "" ) . trim ( ) ,
196+ lastName : ( currentFields . lastName || "" ) . trim ( ) ,
197+ username : ( currentFields . username || "" ) . trim ( ) ,
198+ email : ( currentFields . email || "" ) . trim ( ) ,
202199 } ;
203200
204- try {
205- StorageService . save ( "PROFILE_SETTINGS_USER_COPY" , JSON . stringify ( userCopy ) ) ;
206- } catch ( e ) {
207- // ignore
201+ if ( initialFields ) {
202+ const fieldKeys = [ "firstName" , "lastName" , "username" , "email" ] ;
203+ fieldKeys . forEach ( ( key ) => {
204+ if ( trimmedFields [ key ] !== initialFields [ key ] ) {
205+ payload [ key ] = trimmedFields [ key ] ;
206+ }
207+ } ) ;
208208 }
209209
210- onClose ( ) ;
210+ // If language changed, add locale and ensure username is included
211+ if ( currentLang !== prevLang ) {
212+ payload . attributes = { locale : [ currentLang ] } ;
213+ payload . username = payload . username || trimmedFields . username ;
214+ }
215+
216+ return payload ;
217+ } ;
218+
219+ // Helper: Build updated user detail object from API response
220+ const buildUpdatedUserDetail = ( userDetail , responseData , profileData , newLang , hasLangChange ) => {
221+ const updated = {
222+ ...userDetail ,
223+ // Use response data if available
224+ ...( responseData . firstName && { given_name : responseData . firstName } ) ,
225+ ...( responseData . lastName && { family_name : responseData . lastName } ) ,
226+ ...( responseData . email && { email : responseData . email } ) ,
227+ ...( responseData . username && { preferred_username : responseData . username } ) ,
228+ // Fallback to profileData if response doesn't include the fields
229+ ...( ! responseData . firstName && profileData . firstName && { given_name : profileData . firstName } ) ,
230+ ...( ! responseData . lastName && profileData . lastName && { family_name : profileData . lastName } ) ,
231+ ...( ! responseData . email && profileData . email && { email : profileData . email } ) ,
232+ ...( ! responseData . username && profileData . username && { preferred_username : profileData . username } ) ,
233+ } ;
234+
235+ // Update the name field if first or last name changed
236+ if ( profileData . firstName || profileData . lastName ) {
237+ const newFirst = responseData . firstName || profileData . firstName || userDetail . given_name || "" ;
238+ const newLast = responseData . lastName || profileData . lastName || userDetail . family_name || "" ;
239+ updated . name = `${ newFirst } ${ newLast } ` . trim ( ) ;
240+ }
241+
242+ // Update locale if language changed
243+ if ( hasLangChange ) {
244+ updated . locale = newLang ;
245+ }
246+
247+ return updated ;
248+ } ;
249+
250+ // Helper: Apply language change to i18n and localStorage
251+ const applyLanguageChange = ( newLang ) => {
252+ i18n . changeLanguage ( newLang ) ;
253+ localStorage . setItem ( "i18nextLng" , newLang ) ;
254+ } ;
255+
256+ const handleConfirmProfile = async ( ) => {
257+ // Prevent profile updates for SSO/federated users
258+ if ( isSSO ) {
259+ setError ( t ( "Profile editing is disabled for federated login users." ) ) ;
260+ return ;
261+ }
262+
263+ if ( ! userId ) {
264+ setError ( t ( "User ID not found. Please try again." ) ) ;
265+ return ;
266+ }
267+
268+ const hasLanguageChange = selectedLang !== prevSelectedLang ;
269+ const profileData = buildProfilePayload ( profileFields , initialProfileFields , selectedLang , prevSelectedLang ) ;
270+
271+ if ( Object . keys ( profileData ) . length === 0 ) {
272+ onClose ( ) ;
273+ return ;
274+ }
275+
276+ setIsLoading ( true ) ;
277+ setError ( null ) ;
278+
279+ try {
280+ const response = await updateUserProfile ( userId , profileData ) ;
281+ const responseData = response ?. data || { } ;
282+ const userDetail = JSON . parse ( StorageService . get ( StorageService . User . USER_DETAILS ) ) || { } ;
283+
284+ const updatedUserDetail = buildUpdatedUserDetail (
285+ userDetail , responseData , profileData , selectedLang , hasLanguageChange
286+ ) ;
287+
288+ StorageService . save ( StorageService . User . USER_DETAILS , JSON . stringify ( updatedUserDetail ) ) ;
289+
290+ if ( hasLanguageChange ) {
291+ applyLanguageChange ( selectedLang ) ;
292+ }
293+
294+ if ( publish ) {
295+ publish ( "profileUpdated" , { ...responseData , userId } ) ;
296+ }
297+
298+ onClose ( ) ;
299+ } catch ( err ) {
300+ console . error ( "Error updating profile:" , err ) ;
301+ const errorMessage = err ?. response ?. data ?. message || err ?. message || t ( "Failed to update profile. Please try again." ) ;
302+ setError ( errorMessage ) ;
303+ } finally {
304+ setIsLoading ( false ) ;
305+ }
211306 } ;
212307
213308
@@ -321,6 +416,14 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
321416 < Modal . Body >
322417 { activeTab === "Profile" ? (
323418 < >
419+ { isSSO && (
420+ < CustomInfo
421+ className = "mb-3"
422+ variant = "secondary"
423+ icon = { < ApplicationLogo width = "1.1875rem" height = "1.4993rem" /> }
424+ content = { t ( "Your profile is managed by your identity provider. Profile editing is disabled for federated login users." ) }
425+ />
426+ ) }
324427 < div className = "profile-settings-details-box p-3 mb-3 border rounded" >
325428 < div className = "row g-3" >
326429 < div className = "col-12 col-md-6" >
@@ -434,6 +537,7 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
434537 variant = "primary"
435538 className = "mb-3"
436539 onChange = { handleLanguageChange }
540+ disabled = { isSSO }
437541 />
438542 </ >
439543 ) : (
@@ -472,13 +576,18 @@ export const ProfileSettingsModal = ({ show, onClose, tenant, publish }) => {
472576 </ Modal . Body >
473577
474578 < Modal . Footer >
579+ { error && (
580+ < div className = "profile-error-message text-danger mb-2 w-100" >
581+ { error }
582+ </ div >
583+ ) }
475584 < div className = "buttons-row d-flex justify-content-end" >
476585 < V8CustomButton
477- label = { t ( "Update" ) }
586+ label = { isLoading ? t ( "Updating..." ) : t ( "Update" ) }
478587 onClick = { handleConfirmProfile }
479588 dataTestId = "save-profile-settings"
480589 ariaLabel = { t ( "Save Profile Settings" ) }
481- disabled = { activeTab !== "Profile" || ! isAnythingChanged || emailIsInvalid || usernameIsInvalid }
590+ disabled = { activeTab !== "Profile" || ! isAnythingChanged || emailIsInvalid || usernameIsInvalid || isLoading || isSSO }
482591 variant = "primary"
483592 />
484593 </ div >
0 commit comments