/** * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import { AlertLevels, IdentifiableComponentInterface } from "@thiva/core/models"; import { addAlert } from "@thiva/core/store"; import { Field, Form } from "@thiva/form"; import { ConfirmationModal, ContentLoader, CopyInputField, DocumentationLink, EmphasizedSegment, GridLayout, Message, PageLayout, Popup, Text, useDocumentation } from "@thiva/react-components"; import { AxiosError } from "axios"; import React, { FunctionComponent, MutableRefObject, ReactElement, SyntheticEvent, useEffect, useRef, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Checkbox, CheckboxProps, Grid, Icon, List, Ref } from "semantic-ui-react"; import { ApplicationManagementConstants } from "@thiva/admin.applications.v1/constants"; import { AppConstants, AppState, history } from "@thiva/admin.core.v1"; import { useSMSNotificationSenders } from "../../identity-providers/api"; import { updateMyAccountMFAOptions, updateMyAccountStatus, updateTotpConfigOptions, useMyAccountData, useMyAccountStatus, useTotpConfigData } from "../api"; import { CHANNEL_TYPE, MyAccountAttributeTypes, TotpConfigAttributeTypes, VALID_SMS_OTP_PROVIDERS } from "../constants"; import { MyAccountFormInterface, MyAccountPortalStatusInterface, TotpConfigPortalStatusInterface } from "../models"; /** * Props for my account settings page. */ type MyAccountSettingsEditPage = IdentifiableComponentInterface; const FORM_ID: string = "my-account-settings-form"; /** * Governance connector listing page. * * @param props - Props injected to the component. * @returns Governance connector listing page component. */ export const MyAccountSettingsEditPage: FunctionComponent = ( props: MyAccountSettingsEditPage ): ReactElement => { const { [ "data-componentid" ]: componentId } = props; const dispatch: Dispatch = useDispatch(); const pageContextRef: MutableRefObject = useRef(null); const { t } = useTranslation(); const { getLink } = useDocumentation(); const consumerAccountURL: string = useSelector((state: AppState) => state?.config?.deployment?.accountApp?.tenantQualifiedPath); const [ isMyAccountEnabled, setMyAccountEnabled ] = useState(AppConstants.DEFAULT_MY_ACCOUNT_STATUS); const [ isLoadingForTheFirstTime, setIsLoadingForTheFirstTime ] = useState(true); const [ showMyAccountStatusEnableModal, setShowMyAccountStatusEnableConfirmationModal ] = useState(false); const [ showMyAccountStatusDisableModal, setShowMyAccountStatusDisableConfirmationModal ] = useState(false); const [ isSubmitting, setSubmitting ] = useState(false); const [ initialFormValues, setInitialFormValues ] = useState(undefined); const [ isApplicationRedirect, setApplicationRedirect ] = useState(false); const [ isSmsOtpEnabled, setIsSmsOtpEnabled ] = useState(false); const [ isOtpEnabled, setIsOtpEnabled ] = useState(false); const [ isTOTPChecked, setIsTOTPChecked ] = useState(false); const [ isEmailOtpChecked, setIsEmailOtpChecked ] = useState(false); const [ isSmsOtpChecked, setIsSmsOtpChecked ] = useState(false); const [ checkBackupCodeAuthenticator, setCheckBackupCodeAuthenticator ] = useState(false); const [ checkTotpEnrollment, setCheckTotpEnrollment ] = useState(false); const { data: myAccountStatus, isLoading: isMyAccountStatusLoading, error: myAccountStatusFetchRequestError, mutate: mutateMyAccountStatusFetchRequest } = useMyAccountStatus(); const { data: myAccountData, isLoading: isMyAccountDataLoading, error: myAccountDataFetchRequestError, isValidating: isMyAccountDataValidating, mutate: mutateMyAccountDataFetchRequest } = useMyAccountData(); const { data: totpConfigData, isLoading: totpConfigDataLoading, error: totpConfigDataFetchRequestError, isValidating: totpConfigDataValidating, mutate: mutateTotpConfigDataFetchRequest } = useTotpConfigData(); const { data: notificationSendersList, error: notificationSendersListFetchRequestError } = useSMSNotificationSenders(); useEffect(() => { const locationState: unknown = history.location.state; if (locationState === ApplicationManagementConstants.APPLICATION_STATE) { setApplicationRedirect(true); } }, []); /** * Handles the my account status fetch request error. */ useEffect(() => { if (!myAccountStatusFetchRequestError) { return; } if (myAccountStatusFetchRequestError.response && myAccountStatusFetchRequestError.response.data && myAccountStatusFetchRequestError.response.data.description) { if (myAccountStatusFetchRequestError.response.status === 404) { return; } dispatch(addAlert({ description: myAccountStatusFetchRequestError.response.data.description ?? t("applications:myaccount.fetchMyAccountStatus.error.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountStatus.error.message") })); return; } dispatch(addAlert({ description: t("applications:myaccount.fetchMyAccountStatus" + ".genericError.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountStatus" + ".genericError.message") })); }, [ myAccountStatusFetchRequestError ]); /** * Handles the my account data fetch request error. */ useEffect(() => { if (!myAccountDataFetchRequestError) { return; } if (myAccountDataFetchRequestError.response && myAccountDataFetchRequestError.response.data && myAccountDataFetchRequestError.response.data.description) { if (myAccountDataFetchRequestError.response.status === 404) { return; } dispatch(addAlert({ description: myAccountDataFetchRequestError.response.data.description ?? t("applications:myaccount.fetchMyAccountData.error.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountData.error.message") })); return; } dispatch(addAlert({ description: t("applications:myaccount.fetchMyAccountData" + ".genericError.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountData" + ".genericError.message") })); }, [ myAccountDataFetchRequestError ]); /** * Handles the my account data fetch request error. */ useEffect(() => { if (!totpConfigDataFetchRequestError) { return; } if (totpConfigDataFetchRequestError.response?.data?.description) { if (totpConfigDataFetchRequestError.response.status === 404) { return; } dispatch(addAlert({ description: totpConfigDataFetchRequestError.response.data.description ?? t("applications:myaccount.fetchMyAccountData.error.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountData.error.message") })); return; } dispatch(addAlert({ description: t("applications:myaccount.fetchMyAccountData" + ".genericError.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.fetchMyAccountData" + ".genericError.message") })); }, [ totpConfigDataFetchRequestError ]); /** * Sets the initial spinner. * TODO: Remove this once the loaders are finalized. */ useEffect(() => { if (isMyAccountStatusLoading === false && isLoadingForTheFirstTime === true) { let status: boolean = AppConstants.DEFAULT_MY_ACCOUNT_STATUS; if (myAccountStatus) { const enableProperty: string = myAccountStatus["value"]; if (enableProperty && enableProperty === "false") { status = false; } } setMyAccountEnabled(status); setIsLoadingForTheFirstTime(false); } initializeForm(); }, [ isMyAccountDataLoading, isMyAccountStatusLoading, isLoadingForTheFirstTime, totpConfigDataValidating, isMyAccountDataValidating ]); useEffect(() => { if (!notificationSendersListFetchRequestError) { if (notificationSendersList) { let enableSMSOTP: boolean = false; for (const notificationSender of notificationSendersList) { if (notificationSender.name === CHANNEL_TYPE && VALID_SMS_OTP_PROVIDERS.includes(notificationSender.provider)) { enableSMSOTP = true; break; } } setIsSmsOtpEnabled(enableSMSOTP); } } else { dispatch(addAlert({ description: t("extensions:develop.identityProviders.smsOTP.settings.errorNotifications" + ".notificationSendersRetrievalError.description"), level: AlertLevels.ERROR, message:t("extensions:develop.identityProviders.smsOTP.settings.errorNotifications" + ".notificationSendersRetrievalError.message") })); } }, [ notificationSendersList, notificationSendersListFetchRequestError ]); /** * Handles the `isOtpEnabled` state change when the OTP methods are changed. */ useEffect(() => { if (isTOTPChecked || isEmailOtpChecked || isSmsOtpChecked) { setIsOtpEnabled(true); } else { setIsOtpEnabled(false); setCheckBackupCodeAuthenticator(false); } }, [ isTOTPChecked, isEmailOtpChecked, isSmsOtpChecked ]); useEffect(() => { if (!isTOTPChecked) { setCheckTotpEnrollment(false); } }, [ isTOTPChecked ]); const getMyAccountStatus = (attributeName: string): boolean => { const status: MyAccountPortalStatusInterface[] = myAccountData ?.attributes?.filter((attribute: MyAccountPortalStatusInterface) => attribute.key === attributeName); if (status?.length > 0) { // This will return false only if the value is false. // Otherwise, it will always return true. return status[0]?.value?.toLowerCase() === "true"; } return false; }; const getTotpConfigData = (attributeName: string): boolean => { const status: TotpConfigPortalStatusInterface[] = totpConfigData ?.attributes?.filter((attribute: TotpConfigPortalStatusInterface) => attribute.key === attributeName); if (status?.length > 0) { // This will return false only if the value is false. // Otherwise, it will always return true. return status[0]?.value?.toLowerCase() === "true"; } return false; }; const initializeForm = (): void => { const isEmailOtpEnabled: boolean = getMyAccountStatus(MyAccountAttributeTypes.EMAIL_OTP_ENABLED); const isSmsOtpEnabled: boolean = getMyAccountStatus(MyAccountAttributeTypes.SMS_OTP_ENABLED); const isTotpEnabled: boolean = getMyAccountStatus(MyAccountAttributeTypes.TOTP_ENABLED); let isBackupCodeEnabled: boolean = getMyAccountStatus(MyAccountAttributeTypes.BACKUP_CODE_ENABLED); let totpEnrollmentEnabled: boolean = getTotpConfigData( TotpConfigAttributeTypes.ENROLL_USER_IN_AUTHENTICATION_FLOW); setIsTOTPChecked(isTotpEnabled); setIsEmailOtpChecked(isEmailOtpEnabled); setIsSmsOtpChecked(isSmsOtpEnabled); if (!isTotpEnabled) { totpEnrollmentEnabled = false; } if (isEmailOtpEnabled || isSmsOtpEnabled || isTotpEnabled) { setIsOtpEnabled(true); } else { setIsOtpEnabled(false); isBackupCodeEnabled = false; } setCheckBackupCodeAuthenticator(isBackupCodeEnabled); setCheckTotpEnrollment(totpEnrollmentEnabled); setInitialFormValues({ backupCodeEnabled: isBackupCodeEnabled, emailOtpEnabled: isEmailOtpEnabled, smsOtpEnabled: isSmsOtpEnabled, totpEnabled: isTotpEnabled, totpEnrollmentEnabled: totpEnrollmentEnabled }); }; /** * Handle back button click. */ const handleBackButtonClick = () => { if (isApplicationRedirect) { history.push(AppConstants.getPaths().get("APPLICATIONS")); return; } history.push(AppConstants.getPaths().get("MY_ACCOUNT")); }; /** * Handles the My Account Portal status update action. * * @param e - SyntheticEvent of My Account toggle. * @param data - CheckboxProps of My Account toggle. */ const handleMyAccountStatusToggle = (e: SyntheticEvent, data: CheckboxProps): void => { if (data.checked) { setShowMyAccountStatusEnableConfirmationModal(true); } else { setShowMyAccountStatusDisableConfirmationModal(true); } }; /** * Renders a confirmation modal when the My Account Portal status is being enabled. * @returns My Account status enabling warning modal. */ const renderMyAccountStatusEnableWarning = (): ReactElement => { return ( setShowMyAccountStatusEnableConfirmationModal(false) } type="warning" open={ showMyAccountStatusEnableModal } primaryAction={ t("common:confirm") } secondaryAction={ t("common:cancel") } onSecondaryActionClick={ (): void => { setShowMyAccountStatusEnableConfirmationModal(false); } } onPrimaryActionClick={ (): void => { setShowMyAccountStatusEnableConfirmationModal(false); handleUpdateMyAccountStatus(true); } } closeOnDimmerClick={ false } > { t("applications:myaccount.Confirmation.enableConfirmation.heading") } { t("applications:myaccount.Confirmation.enableConfirmation.message") } { t("applications:myaccount.Confirmation.enableConfirmation.content") } ); }; /** * Renders a confirmation modal when the My Account Portal status is being disabled. * @returns My Account status disabling warning modal. */ const renderMyAccountStatusDisableWarning = (): ReactElement => { return ( setShowMyAccountStatusDisableConfirmationModal(false) } type="warning" open={ showMyAccountStatusDisableModal } primaryAction={ t("common:confirm") } secondaryAction={ t("common:cancel") } onSecondaryActionClick={ (): void => { setShowMyAccountStatusDisableConfirmationModal(false); } } onPrimaryActionClick={ (): void => { setShowMyAccountStatusDisableConfirmationModal(false); handleUpdateMyAccountStatus(false); } } closeOnDimmerClick={ false } > { t("applications:myaccount.Confirmation.disableConfirmation.heading") } { t("applications:myaccount.Confirmation.disableConfirmation.message") } { t("applications:myaccount.Confirmation.disableConfirmation.content") } ); }; /** * Update the My Account Portal status. * * @param status - New status of the My Account portal. */ const handleUpdateMyAccountStatus = (status: boolean): void => { setSubmitting(true); updateMyAccountStatus(status) .then(() => { setMyAccountEnabled(status); mutateMyAccountStatusFetchRequest(); dispatch(addAlert({ description: t("applications:myaccount.notifications.success.description"), level: AlertLevels.SUCCESS, message: t("applications:myaccount.notifications.success.message") })); }).catch((error: AxiosError) => { if (error?.response?.data?.description) { dispatch(addAlert({ description: error?.response?.data?.description ?? error?.response?.data?.detail ?? t("applications:myaccount.notifications.error.description"), level: AlertLevels.ERROR, message: error?.response?.data?.message ?? t("applications:myaccount.notifications.error.message") })); return; } dispatch(addAlert({ description: t( "applications:myaccount.notifications.genericError.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.notifications.genericError.message") })); }).finally(() => { setSubmitting(false); }); }; /** * Update the My Account Portal Data. * * @param values - New data of the My Account portal. */ const handleUpdateMyAccountData = (values: MyAccountFormInterface): void => { setSubmitting(true); values.backupCodeEnabled = checkBackupCodeAuthenticator; values.totpEnrollmentEnabled = checkTotpEnrollment; Promise.all([ updateMyAccountMFAOptions(values), updateTotpConfigOptions(values) ]).then(() => { mutateMyAccountDataFetchRequest(); mutateTotpConfigDataFetchRequest(); dispatch(addAlert({ description: t("applications:myaccount.notifications.success.description"), level: AlertLevels.SUCCESS, message: t("applications:myaccount.notifications.success.message") })); }) .catch((error: AxiosError) => { if (error?.response?.data?.description) { dispatch(addAlert({ description: error?.response?.data?.description ?? error?.response?.data?.detail ?? t("applications:myaccount.notifications.error.description"), level: AlertLevels.ERROR, message: error?.response?.data?.message ?? t("applications:myaccount.notifications.error.message") })); return; } dispatch(addAlert({ description: t( "applications:myaccount.notifications.genericError.description"), level: AlertLevels.ERROR, message: t("applications:myaccount.notifications.genericError.message") })); }) .finally(() => { setSubmitting(false); }); }; /** * Renders the URL for the tenanted my account login. * * @returns My Account link. */ const renderTenantedMyAccountLink = (): ReactElement => { if (!isMyAccountEnabled) { return null; } return ( { t("extensions:manage.myAccount.editPage.myAccountUrlDescription") } ) } content={ t("extensions:manage.myAccount.editPage.myAccountUrlDescription") } position="top center" size="mini" hideOnScroll inverted /> ); }; return !isMyAccountDataValidating ? ( { t("extensions:manage.myAccount.editPage.description") } { t("common:learnMore") } ) } data-componentid={ `${ componentId }-page-layout` } backButton={ { "data-testid": `${ componentId }-page-back-button`, onClick: handleBackButtonClick, text: isApplicationRedirect ? t("extensions:manage.myAccount.goBackToApplication") : t("extensions:manage.myAccount.goBackToMyAccount") } } bottomMargin={ false } contentTopMargin={ true } pageHeaderMaxWidth={ true } > { renderTenantedMyAccountLink() } { (!isMyAccountDataLoading && !totpConfigDataLoading) ? (
handleUpdateMyAccountData(values) } > { t("extensions:manage.myAccount.editPage.mfaDescription") } setIsTOTPChecked(value) } data-testid={ `${ componentId }-totp-toggle` } />
setCheckTotpEnrollment(value) } data-testid={ `${ componentId }-totp-enrollment-toggle` } checked={ checkTotpEnrollment } />
{ t("extensions:manage.myAccount.editPage.totpEnrollmentInfo") } setIsEmailOtpChecked(value) } data-testid={ `${ componentId }-email-otp-toggle` } /> setIsSmsOtpChecked(value) } data-testid={ `${ componentId }-sms-otp-toggle` } /> { !isSmsOtpEnabled && ( To enable the SMS OTP authentication option, you need to set up the SMS OTP authenticator for your organization. Learn more ) }
{ t("extensions:manage.myAccount.editPage.backupCodeDescription") }
setCheckBackupCodeAuthenticator(value) } checked={ checkBackupCodeAuthenticator } />
{ !isOtpEnabled && (
) }
{ showMyAccountStatusEnableModal && renderMyAccountStatusEnableWarning() } { showMyAccountStatusDisableModal && renderMyAccountStatusDisableWarning() }
) : ( ); }; /** * Default props for the component. */ MyAccountSettingsEditPage.defaultProps = { "data-componentid": "my-account-settings-edit-page" }; /** * A default export was added to support React.lazy. * TODO: Change this to a named export once react starts supporting named exports for code splitting. * @see {@link https://reactjs.org/docs/code-splitting.html#reactlazy} */ export default MyAccountSettingsEditPage;