Repository: wso2/identity-apps
Pull request #7901: Password recovery Email OTP support
Author: login: Malith-19
Created: 2025-03-18T02:51:01Z | Merged: 2025-03-24T02:39:32Z

### Purpose
- $subject

### Description
- Extended the console page setting to select option when email method is selected. (email link will be selected in default)
- In this effort I have extended the current sms-otp.jsp to support both email and sms.
- Additinaly we have add the email-otp variation to the branding page.
- Add email template mappings to the `deployment.config.json`

### Related Issues
- https://github.com/wso2/product-is/issues/23444

### Related PRs
- https://github.com/wso2-extensions/identity-governance/pull/929
- https://github.com/wso2/carbon-identity-framework/pull/6614
- https://github.com/wso2-extensions/identity-event-handler-notification/pull/312

### Checklist

- [ ] e2e cypress tests locally verified. (for internal contributers)
- [x] Manual test round performed and verified.
- [ ] UX/UI review done on the final implementation.
- [ ] Documentation provided. (Add links if there are any)
- [ ] Relevant backend changes deployed and verified
- [ ] [Unit tests](/docs/testing/UNIT_TESTING.md) provided. (Add links if there are any)
- [ ] [Integration tests](https://github.com/wso2/product-is/tree/master/modules/integration) provided. (Add links if there are any)

### Security checks
- [ ] Followed secure coding standards in http://wso2.com/technical-reports/wso2-secure-engineering-guidelines
- [ ] Ran FindSecurityBugs plugin and verified report
- [x] Confirmed that this PR doesn't commit any keys, passwords, tokens, usernames, or other secrets

### Previews (click on the images for better view)

#### Password form page
<img width="611" alt="image" src="https://github.com/user-attachments/assets/c3c9939d-73cb-49e3-8ebc-5ca4c79e39ef" />

#### Email OTP page
<img width="630" alt="image" src="https://github.com/user-attachments/assets/d3bf08e0-3f33-4194-b917-9a3e2142b53e" />

#### Console setting page
![Screenshot 2025-03-14 at 17 11 18](https://github.com/user-attachments/assets/dc126e74-5238-481f-8fdc-a2ce0ad0fee7)




Diff:
diff --git a/.changeset/forty-coins-cross.md b/.changeset/forty-coins-cross.md
new file mode 100644
index 00000000000..ec032c95001
--- /dev/null
+++ b/.changeset/forty-coins-cross.md
@@ -0,0 +1,10 @@
+---
+"@wso2is/admin.server-configurations.v1": patch
+"@wso2is/common.branding.v1": minor
+"@wso2is/admin.branding.v1": minor
+"@wso2is/identity-apps-core": minor
+"@wso2is/console": minor
+"@wso2is/i18n": patch
+---
+
+Add password recovery email otp support.
diff --git a/apps/console/src/extensions/i18n/models/extensions.ts b/apps/console/src/extensions/i18n/models/extensions.ts
index c8923e2f6ba..17456027749 100755
--- a/apps/console/src/extensions/i18n/models/extensions.ts
+++ b/apps/console/src/extensions/i18n/models/extensions.ts
@@ -2841,10 +2841,19 @@ export interface Extensions {
                             passwordRecoveryOtpUseLowercase: FormAttributes;
                             passwordRecoveryOtpUseNumeric: FormAttributes;
                             passwordRecoveryOtpLength: FormAttributes;
+                            emailRecoveryOptions: {
+                                header: string;
+                                emailLink: {
+                                    label: string;
+                                },
+                                emailOtp: {
+                                    label: string;
+                                }
+                            };
                         };
                         smsProviderWarning: string;
                     };
-                    recoveryOptionSubHeadingEmailLink: string;
+                    recoveryOptionSubHeadingEmail: string;
                     recoveryOptionSubHeadingSMS: string;
                     recoveryOptionHeading: string;
                     otpConfigHeading: string;
diff --git a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts
index 45dae964411..b385fc6482b 100755
--- a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts
+++ b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts
@@ -3222,17 +3222,17 @@ export const extensions: Extensions = {
                                 label: "Enable SMS based recovery"
                             },
                             expiryTime: {
-                                hint: "Password recovery link expiry time in minutes.",
-                                label: "Recovery link expiry time",
+                                hint: "Password recovery link/OTP expiry time in minutes.",
+                                label: "Recovery link/OTP expiry time",
                                 placeholder: "Enter expiry time",
                                 validations: {
-                                    invalid: "Recovery link expiry time should be an integer.",
-                                    empty: "Recovery link expiry time cannot be empty.",
+                                    invalid: "Recovery link/OTP expiry time should be an integer.",
+                                    empty: "Recovery link/OTP expiry time cannot be empty.",
                                     range:
-                                        "Recovery link expiry time should be between 1 minute & 10080 minutes " +
+                                        "Recovery link/OTP expiry time should be between 1 minute & 10080 minutes " +
                                         "(7 days).",
                                     maxLengthReached:
-                                        "Recovery link expiry time should be a number with 5 or less " + "digits."
+                                        "Recovery link/OTP expiry time should be a number with 5 or less " + "digits."
                                 }
                             },
                             notifySuccess: {
@@ -3304,14 +3304,23 @@ export const extensions: Extensions = {
                                 }
                             },
                             enableEmailBasedRecovery: {
-                                hint: "This specifies whether to send an recovery link to the email address.",
-                                label: "Enable email link based recovery"
+                                hint: "This specifies whether to send an recovery link/OTP to the email address.",
+                                label: "Enable email based recovery"
+                            },
+                            emailRecoveryOptions: {
+                                header: "Choose Email recovery option",
+                                emailLink: {
+                                    label: "Email Link"
+                                },
+                                emailOtp: {
+                                    label: "Email OTP"
+                                }
                             }
                         },
                         smsProviderWarning:
                             "Ensure that an <1>SMS Provider</1> is configured for the OTP feature to work properly."
                     },
-                    recoveryOptionSubHeadingEmailLink: "Email Link",
+                    recoveryOptionSubHeadingEmail: "Email",
                     recoveryOptionSubHeadingSMS: "SMS OTP",
                     recoveryOptionHeading: "Recovery Option Selection",
                     otpConfigHeading: "OTP Code Configuration",
diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json
index c1c10ac7f4d..9d41bc2ebf6 100644
--- a/apps/console/src/public/deployment.config.json
+++ b/apps/console/src/public/deployment.config.json
@@ -120,6 +120,16 @@
                 "description": "This email informs the user that the email address is updated",
                 "displayName": "Email Update Notification",
                 "id": "bm90aWZ5T25FeGlzdGluZ0VtYWlsVXBkYXRl"
+            },
+            {
+                "description": "This email sends an Email OTP to a user for password reset",
+                "displayName": "Password Reset Email OTP",
+                "id": "UGFzc3dvcmRSZXNldE9UUA"
+            },
+            {
+                "description": "This email resends an Email OTP to a user for password reset",
+                "displayName": "Resend Password Reset Email OTP",
+                "id": "cmVzZW5kUGFzc3dvcmRSZXNldE9UUA"
             }
         ],
         "outboundProvisioningConnectors": [],
diff --git a/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-email-otp-fragment.tsx b/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-email-otp-fragment.tsx
new file mode 100644
index 00000000000..2b3ec176c46
--- /dev/null
+++ b/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-email-otp-fragment.tsx
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2025, 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 { IdentifiableComponentInterface } from "@wso2is/core/models";
+import React, { FunctionComponent, ReactElement } from "react";
+import { CustomTextPreferenceConstants } from "../../../../../constants/custom-text-preference-constants";
+import useBrandingPreference from "../../../../../hooks/use-branding-preference";
+
+/**
+ * Proptypes for the password-recovery fragment of login screen skeleton.
+ */
+export type PasswordRecoveryEmailOTPFragmentInterface = IdentifiableComponentInterface;
+
+/**
+ * Password recovery fragment component for the branding preview of Password Recovery box.
+ *
+ * @param props - Props injected to the component.
+ * @returns Password recovery fragment component.
+ */
+const PasswordRecoveryEmailOTPFragment: FunctionComponent<PasswordRecoveryEmailOTPFragmentInterface> = ({
+    ["data-componentid"]: componentId = "branding-preview-password-recovery-fragment"
+} : PasswordRecoveryEmailOTPFragmentInterface): ReactElement => {
+
+    const { i18n } = useBrandingPreference();
+
+    return (
+        <div data-componentid={ componentId }>
+            <h2>
+                { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.HEADING, "Forgot Password?") }
+            </h2>
+            <div className="segment-form">
+                <form method="post" id="otpForm" className="ui large form otp-form">
+                    <p className="line-break text-center" id="instruction" >
+                        { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BODY,
+                            "Don't worry, it happens. We will send you an email to reset your password.") }
+                    </p>
+                    <div className="ui divider hidden"></div>
+                    <div className="ui large form">
+                        <div className="field">
+                            <div className="ui fluid left icon input">
+                                <input
+                                    type="text"
+                                    id="usernameUserInput"
+                                    name="usernameUserInput"
+                                    placeholder={
+                                        i18n(CustomTextPreferenceConstants
+                                            .TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY
+                                            .IDENTIFIER.INPUT.PLACEHOLDER, "Username")
+                                    }
+                                    data-componentid="login-page-username-input"
+                                />
+                                <i aria-hidden="true" className="user outline icon"></i>
+                                <input id="username" name="username" type="hidden" />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="ui divider hidden"></div>
+                    <div>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui primary fluid large button mb-2"
+                        >
+                            { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BUTTON.EMAIL_OTP,
+                                "Send Email OTP") }
+                        </button>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui secondary fluid large button"
+                        >
+                            Cancel
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    );
+};
+
+export default PasswordRecoveryEmailOTPFragment;
diff --git a/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-multi-option-fragment.tsx b/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-multi-option-fragment.tsx
index 4732f784260..f62244d4953 100644
--- a/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-multi-option-fragment.tsx
+++ b/features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-multi-option-fragment.tsx
@@ -91,7 +91,7 @@ const PasswordRecoveryMultiOptionFragment: FunctionComponent<PasswordRecoveryMul
                                 <input type="radio" name="recoveryOption" value="SMSOTP" />
                                 <label>{ i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY
                                     .RADIO_BUTTON.SMS_OTP,
-                                "Send code via SMS.") }
+                                "Send OTP via SMS.") }
                                 </label>
                             </div>
                         </div>
diff --git a/features/admin.branding.v1/components/preview/sign-in-box/sign-in-box.tsx b/features/admin.branding.v1/components/preview/sign-in-box/sign-in-box.tsx
index 92eba23da44..ee1aa9c9a39 100644
--- a/features/admin.branding.v1/components/preview/sign-in-box/sign-in-box.tsx
+++ b/features/admin.branding.v1/components/preview/sign-in-box/sign-in-box.tsx
@@ -31,6 +31,7 @@ import CommonFragment from "./fragments/common-fragment";
 import EmailLinkExpiryFragment from "./fragments/email-link-expiry-fragment";
 import EmailOTPFragment from "./fragments/email-otp-fragment";
 import PasswordRecoveryEmailLinkFragment from "./fragments/password-recovery/password-recovery-email-link-fragment";
+import PasswordRecoveryEmailOTPFragment from "./fragments/password-recovery/password-recovery-email-otp-fragment";
 import PasswordRecoveryMultiOptionFragment from "./fragments/password-recovery/password-recovery-multi-option-fragment";
 import PasswordRecoverySMSFragment from "./fragments/password-recovery/password-recovery-sms-otp-fragment";
 import PasswordResetFragment from "./fragments/password-reset-fragment";
@@ -95,7 +96,10 @@ const SignInBox: FunctionComponent<SignInBoxInterface> = (
                 selectedScreenVariation === PreviewScreenVariationType.BASE
             ) {
                 return <PasswordRecoveryEmailLinkFragment />;
-            } else if(selectedScreenVariation === PreviewScreenVariationType.SMS_OTP) {
+            } else if (selectedScreenVariation === PreviewScreenVariationType.EMAIL_OTP) {
+                return <PasswordRecoveryEmailOTPFragment />;
+            }
+            else if(selectedScreenVariation === PreviewScreenVariationType.SMS_OTP) {
                 return <PasswordRecoverySMSFragment/>;
             } else if(selectedScreenVariation === PreviewScreenVariationType.MULTI) {
                 return <PasswordRecoveryMultiOptionFragment/>;
diff --git a/features/admin.branding.v1/constants/custom-text-preference-constants.ts b/features/admin.branding.v1/constants/custom-text-preference-constants.ts
index da7a24b0d37..5636613a7f8 100644
--- a/features/admin.branding.v1/constants/custom-text-preference-constants.ts
+++ b/features/admin.branding.v1/constants/custom-text-preference-constants.ts
@@ -168,6 +168,7 @@ export class CustomTextPreferenceConstants {
             BUTTON: {
                 SMS_OTP: string,
                 EMAIL_LINK: string,
+                EMAIL_OTP: string,
                 MULTI: string
             };
         },
@@ -221,6 +222,7 @@ export class CustomTextPreferenceConstants {
                 BODY: "password.recovery.body",
                 BUTTON: {
                     EMAIL_LINK: "password.recovery.button.email.link",
+                    EMAIL_OTP: "password.recovery.button.email.otp",
                     MULTI: "password.recovery.button.multi",
                     SMS_OTP: "password.recovery.button.smsotp"
                 },
diff --git a/features/admin.server-configurations.v1/constants/password-recovery-constants.ts b/features/admin.server-configurations.v1/constants/password-recovery-constants.ts
index 10b4bbb1f61..fb084ef78cc 100644
--- a/features/admin.server-configurations.v1/constants/password-recovery-constants.ts
+++ b/features/admin.server-configurations.v1/constants/password-recovery-constants.ts
@@ -31,6 +31,7 @@ export class PasswordRecoveryFormConstants {
         ServerConfigurationsConstants.NOTIFY_SUCCESS,
         ServerConfigurationsConstants.RECOVERY_LINK_EXPIRY_TIME,
         ServerConfigurationsConstants.RECOVERY_EMAIL_LINK_ENABLE,
+        ServerConfigurationsConstants.RECOVERY_EMAIL_OTP_ENABLE,
         ServerConfigurationsConstants.RECOVERY_SMS_OTP_ENABLE,
         ServerConfigurationsConstants.RECOVERY_SMS_EXPIRY_TIME,
         ServerConfigurationsConstants.RECOVERY_OTP_USE_UPPERCASE,
diff --git a/features/admin.server-configurations.v1/constants/server-configurations-constants.ts b/features/admin.server-configurations.v1/constants/server-configurations-constants.ts
index bcfdd69c2b7..3c5c8d8c262 100644
--- a/features/admin.server-configurations.v1/constants/server-configurations-constants.ts
+++ b/features/admin.server-configurations.v1/constants/server-configurations-constants.ts
@@ -243,6 +243,7 @@ export class ServerConfigurationsConstants {
         "Recovery.Question.Password.Forced.Enable";
 
     public static readonly RECOVERY_EMAIL_LINK_ENABLE: string = "Recovery.Notification.Password.emailLink.Enable";
+    public static readonly RECOVERY_EMAIL_OTP_ENABLE: string = "Recovery.Notification.Password.OTP.SendOTPInEmail";
     public static readonly RECOVERY_SMS_OTP_ENABLE: string = "Recovery.Notification.Password.smsOtp.Enable";
     public static readonly RECOVERY_OTP_USE_UPPERCASE: string =
         "Recovery.Notification.Password.OTP.UseUppercaseCharactersInOTP";
diff --git a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss
index 21ef12eb6ef..e9db4c87845 100644
--- a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss
+++ b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss
@@ -26,6 +26,10 @@
         margin-bottom: 15px;
     }
 
+    h6.ui.header.heading {
+        margin-bottom: 10px;
+    }
+
     .ui-hint {
         margin-bottom: 25px;
     }
@@ -34,8 +38,12 @@
         div:nth-child(6),
         div:nth-child(7),
         div:nth-child(8),
-        div:nth-child(13),
-        div:nth-child(14) {
+        div:nth-child(9),
+        div:nth-child(10),
+        div:nth-child(11),
+        div:nth-child(12),
+        div:nth-child(17),
+        div:nth-child(18) {
         margin-left: 50px;
     }
 
diff --git a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
index 72e40323a56..5814efbc9df 100644
--- a/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
+++ b/features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
@@ -22,6 +22,7 @@ import { history } from "@wso2is/admin.core.v1/helpers/history";
 import { AppState } from "@wso2is/admin.core.v1/store";
 import { FeatureStatusLabel } from "@wso2is/admin.feature-gate.v1/models/feature-status";
 import {
+    EmailRecoveryOption,
     PasswordRecoveryConfigurationFormPropsInterface,
     PasswordRecoveryFormConstants,
     PasswordRecoveryFormErrorValidationsInterface,
@@ -30,6 +31,7 @@ import {
 } from "@wso2is/admin.server-configurations.v1";
 import { CommonUtils } from "@wso2is/core/utils";
 import { Field, Form } from "@wso2is/form";
+import { RadioChild } from "@wso2is/forms";
 import { Heading, Hint, Link, Message } from "@wso2is/react-components";
 import { FormValidation } from "@wso2is/validation";
 import isEmpty from "lodash-es/isEmpty";
@@ -72,6 +74,20 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild[] = [
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailLink.label",
+            value: EmailRecoveryOption.EMAIL_LINK
+        },
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailOtp.label",
+            value: EmailRecoveryOption.EMAIL_OTP
+        }
+    ];
 
     const showSmsOtpPwdRecoveryFeatureStatusChip: boolean =
         useSelector((state: AppState) => state?.config?.ui?.showSmsOtpPwdRecoveryFeatureStatusChip);
@@ -102,11 +118,18 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                             expiryTime: property.value
                         };
 
+                        break;
+                    case ServerConfigurationsConstants.RECOVERY_EMAIL_OTP_ENABLE:
+                        resolvedInitialValues = {
+                            ...resolvedInitialValues,
+                            enableEmailOtpBasedRecovery: CommonUtils.parseBoolean(property.value)
+                        };
+
                         break;
                     case ServerConfigurationsConstants.RECOVERY_EMAIL_LINK_ENABLE:
                         resolvedInitialValues = {
                             ...resolvedInitialValues,
-                            enableEmailBasedRecovery: CommonUtils.parseBoolean(property.value)
+                            enableEmailLinkBasedRecovery: CommonUtils.parseBoolean(property.value)
                         };
 
                         break;
@@ -173,7 +196,10 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
             }
         });
         setInitialConnectorValues(resolvedInitialValues);
-        setIsEmailRecoveryEnabled(resolvedInitialValues?.enableEmailBasedRecovery);
+        setIsEmailRecoveryEnabled(resolvedInitialValues?.enableEmailLinkBasedRecovery ||
+             resolvedInitialValues?.enableEmailOtpBasedRecovery);
+        setEmailRecoveryOption(resolvedInitialValues?.enableEmailOtpBasedRecovery ?
+            EmailRecoveryOption.EMAIL_OTP : EmailRecoveryOption.EMAIL_LINK);
         setIsSMSRecoveryEnabled(resolvedInitialValues?.enableSMSBasedRecovery);
         setIsUpperCaseEnabled(resolvedInitialValues?.passwordRecoveryOtpUseUppercase);
         setIsLowerCaseEnabled(resolvedInitialValues?.passwordRecoveryOtpUseLowercase);
@@ -283,6 +309,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
      * @returns Sanitized form values.
      */
     const getUpdatedConfigurations = (values: Record<string, any>) => {
+
         const data: PasswordRecoveryFormUpdatableConfigsInterface = {
             "Recovery.ExpiryTime": values.expiryTime !== undefined
                 ? values.expiryTime
@@ -299,6 +326,9 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
             "Recovery.Notification.Password.OTP.OTPLength": values.smsOtpLength !== undefined
                 ? values.smsOtpLength
                 : initialConnectorValues?.smsOtpLength,
+            "Recovery.Notification.Password.OTP.SendOTPInEmail": emailRecoveryOption !== undefined
+                ? (emailRecoveryOption === EmailRecoveryOption.EMAIL_OTP && values.enableEmailBasedRecovery)
+                : initialConnectorValues?.enableEmailOtpBasedRecovery,
             "Recovery.Notification.Password.OTP.UseLowercaseCharactersInOTP":
                 values.passwordRecoveryOtpUseLowercase !== undefined
                     ? values.passwordRecoveryOtpUseLowercase
@@ -310,9 +340,9 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                 values.passwordRecoveryOtpUseUppercase !== undefined
                     ? values.passwordRecoveryOtpUseUppercase
                     : initialConnectorValues?.passwordRecoveryOtpUseUppercase,
-            "Recovery.Notification.Password.emailLink.Enable": values.enableEmailBasedRecovery !== undefined
-                ? !!values.enableEmailBasedRecovery
-                : initialConnectorValues?.enableEmailBasedRecovery,
+            "Recovery.Notification.Password.emailLink.Enable": emailRecoveryOption !== undefined
+                ? (emailRecoveryOption === EmailRecoveryOption.EMAIL_LINK && values.enableEmailBasedRecovery)
+                : initialConnectorValues?.enableEmailLinkBasedRecovery,
             "Recovery.Notification.Password.smsOtp.Enable": values.enableSMSBasedRecovery !== undefined
                 ? !!values.enableSMSBasedRecovery
                 : initialConnectorValues?.enableSMSBasedRecovery,
@@ -343,7 +373,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                 </Heading>
                 <Heading as="h5">
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
-                            "passwordRecovery.recoveryOptionSubHeadingEmailLink") as ReactNode }
+                            "passwordRecovery.recoveryOptionSubHeadingEmail") as ReactNode }
                 </Heading>
                 <Field.Checkbox
                     ariaLabel="enableEmailBasedRecovery"
@@ -354,6 +384,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     readOnly={ readOnly }
                     width={ 10 }
                     disabled={ !isConnectorEnabled }
+                    defaultValue={ isEmailRecoveryEnabled }
                     listen={ (value: boolean) => setIsEmailRecoveryEnabled(value) }
                     data-testid={ `${ testId }-email-link-based-recovery` }
                     data-componentid={ `${ testId }-email-link-based-recovery` }
@@ -362,6 +393,27 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") }
+                </Heading>
+                {
+                    EMAIL_RECOVERY_RADIO_OPTIONS.map((option: RadioChild) => (
+                        <Field.Radio
+                            key={ option.value }
+                            ariaLabel={ t(option.label) }
+                            label={ t(option.label) }
+                            name="emailRecoveryOption"
+                            type="radio"
+                            value={ option.value }
+                            checked={ emailRecoveryOption === option.value }
+                            listen={ () => setEmailRecoveryOption(option.value) }
+                            disabled={ !isEmailRecoveryEnabled }
+                            readOnly={ readOnly }
+                        />
+                    ))
+                }
+                <br/>
                 <Field.Checkbox
                     ariaLabel="notifyRecoverySuccess"
                     name="notifySuccess"
diff --git a/features/admin.server-configurations.v1/models/password-recovery.ts b/features/admin.server-configurations.v1/models/password-recovery.ts
index dad0090f317..253b9a521b4 100644
--- a/features/admin.server-configurations.v1/models/password-recovery.ts
+++ b/features/admin.server-configurations.v1/models/password-recovery.ts
@@ -58,9 +58,13 @@ export interface PasswordRecoveryFormValuesInterface {
      */
     notifySuccess: boolean;
     /**
-     * Whether email based recovery is enabled.
+     * Whether email link based recovery is enabled.
      */
-    enableEmailBasedRecovery: boolean;
+    enableEmailLinkBasedRecovery: boolean;
+    /**
+     * Whether email OTP based recovery is enabled.
+     */
+    enableEmailOtpBasedRecovery: boolean;
     /**
      * Whether SMS based recovery is enabled.
      */
@@ -131,6 +135,10 @@ export interface PasswordRecoveryFormUpdatableConfigsInterface {
      * Enable email based recovery.
      */
     "Recovery.Notification.Password.emailLink.Enable": boolean;
+    /**
+     * Enable email OTP based recovery.
+     */
+    "Recovery.Notification.Password.OTP.SendOTPInEmail": boolean;
     /**
      * Expire time for the SMS OTP.
      */
@@ -168,3 +176,8 @@ export interface PasswordRecoveryFormUpdatableConfigsInterface {
      */
     "Recovery.NotifySuccess": boolean;
 }
+
+export enum EmailRecoveryOption {
+    EMAIL_OTP = "emailOTP",
+    EMAIL_LINK = "emailLink"
+}
diff --git a/features/common.branding.v1/models/branding-preferences.ts b/features/common.branding.v1/models/branding-preferences.ts
index 6a65cc956ad..f1c24a20b66 100644
--- a/features/common.branding.v1/models/branding-preferences.ts
+++ b/features/common.branding.v1/models/branding-preferences.ts
@@ -587,6 +587,7 @@ export enum PreviewScreenType {
 export enum PreviewScreenVariationType {
     BASE = "base",
     SMS_OTP = "sms-otp",
+    EMAIL_OTP = "email-otp",
     EMAIL_LINK = "email-link",
     MULTI = "multi",
     EMAIL = "email",
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
index 65ab58c79c5..af660b12c50 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
@@ -42,6 +42,11 @@ sign.up.button=Sign Up
 sms.otp.heading=OTP Verification
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+# EDITABLE=true,SCREEN="email-otp",MULTI_LINE=false
+email.otp.heading=OTP Verification
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false
 password.recovery.heading=Forgot Password?
@@ -51,6 +56,8 @@ password.recovery.body=We've got you covered. Enter your username to begin reset
 password.recovery.identifier.input.placeholder=Username
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="email-link"
 password.recovery.button.email.link=Send Reset Link
+# EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="email-otp"
+password.recovery.button.email.otp=Send Email OTP
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="sms-otp"
 password.recovery.button.smsotp=Send SMS OTP
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="multi"
@@ -58,7 +65,9 @@ password.recovery.button.multi=Proceed
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="multi"
 send.email.link=Send reset link via email
 # EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="multi"
-send.code.via.sms=Send code via SMS
+send.email.otp=Send OTP via email
+# EDITABLE=true,SCREEN="password-recovery",MULTI_LINE=false,VARIATIONS="multi"
+send.code.via.sms=Send OTP via SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -341,8 +350,10 @@ enter.your.email=Enter your email
 enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
-resend.code.success=A new OTP is sent to your mobile.
+resend.code.success=A new OTP has been sent to your mobile.
+resend.code.success.email=A new OTP has been sent to your email.
 enter.code.sent.smsotp=Enter the code sent to your mobile phone
+enter.code.sent.emailotp=Enter the code sent to your email
 sms.otp.resend.code=Resend Code
 sms.otp.submit.button=Continue
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_de_DE.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_de_DE.properties
index dccd2d4b475..f81b4ff88b6 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_de_DE.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_de_DE.properties
@@ -34,14 +34,20 @@ sign.up.button=Registrieren
 sms.otp.heading=OTP -Überprüfung
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=OTP -Überprüfung
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=Passwort vergessen?
 password.recovery.body=Wir geben dir Deckung. Geben Sie Ihren Benutzernamen ein, um mit dem Zurücksetzen Ihres Passworts zu beginnen.
 password.recovery.identifier.input.placeholder=Nutzername
 password.recovery.button.email.link=Reset-Link senden
+password.recovery.button.email.otp=E-Mail-OTP senden
 password.recovery.button.smsotp=SMS-Code senden
 send.email.link=Link zum Zurücksetzen per E-Mail senden
-send.code.via.sms=Code per SMS versenden
+send.email.otp=OTP per E-Mail senden
+send.code.via.sms=Senden Sie OTP per SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -302,8 +308,10 @@ enter.your.email=Geben sie ihre E-Mail Adresse ein
 enter.your.username=Geben Sie Ihren Benutzernamen ein
 error.enter.code=Bitte geben Sie den Code ein!
 choose.other.option=Wählen Sie eine andere Option
-resend.code.success=Ein neues OTP wurde an Ihr Handy gesendet.
+resend.code.success=Ein neues OTP wurde an Ihr Mobiltelefon gesendet.
+resend.code.success.email=Ein neues OTP wurde an Ihre E-Mail-Adresse gesendet.
 enter.code.sent.smsotp=Geben Sie den Code ein, der an Ihr Mobiltelefon gesendet wurde
+enter.code.sent.emailotp=Geben Sie den Code ein, der an Ihre E-Mail gesendet wurde
 sms.otp.resend.code=Code erneut senden
 sms.otp.submit.button=Weiter
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_es_ES.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_es_ES.properties
index c97cbe83f1c..22df1e91089 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_es_ES.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_es_ES.properties
@@ -34,14 +34,20 @@ sign.up.button=Inscribirse
 sms.otp.heading=Verificación OTP
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=Verificación OTP
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=¿Has olvidado tu contraseña?
 password.recovery.body=Te tenemos cubierto. Ingrese su nombre de usuario para comenzar a restablecer su contraseña.
 password.recovery.identifier.input.placeholder=Correo electrónico
 password.recovery.button.email.link=Enviar enlace de reinicio
+password.recovery.button.email.otp=Enviar OTP por correo electrónico
 password.recovery.button.smsotp=Enviar codigo sms
 send.email.link=Enviar enlace de reinicio por correo electrónico
-send.code.via.sms=Enviar código por SMS
+send.email.otp=Enviar OTP por correo electrónico
+send.code.via.sms=Enviar OTP por SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -303,7 +309,9 @@ enter.your.username=Ingrese su nombre de usuario
 error.enter.code=¡Ingrese el código!
 choose.other.option=Elija una opción diferente
 resend.code.success=Se ha enviado un nuevo OTP a tu móvil.
+resend.code.success.email=Se ha enviado un nuevo OTP a tu correo electrónico.
 enter.code.sent.smsotp=Introduce el código enviado a tu teléfono móvil
+enter.code.sent.emailotp=Ingresa el código enviado a tu correo electrónico
 sms.otp.resend.code=Reenviar código
 sms.otp.submit.button=Continuar
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_fr_FR.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_fr_FR.properties
index 64ba7d68e6b..f67ff8c994e 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_fr_FR.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_fr_FR.properties
@@ -34,14 +34,20 @@ sign.up.button=S'inscrire
 sms.otp.heading=Vérification de l'OTP
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=Vérification de l'OTP
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=Mot de passe oublié?
 password.recovery.body=Nous avons ce qu'il vous faut. Entrez votre nom d'utilisateur pour commencer à réinitialiser votre mot de passe.
 password.recovery.identifier.input.placeholder=Nom d'utilisateur
 password.recovery.button.email.link=Envoyer le lien de réinitialisation
+password.recovery.button.email.otp=Envoyer un e-mail OTP
 password.recovery.button.smsotp=Envoyer le code SMS
 send.email.link=Envoyer le lien de réinitialisation par e-mail
-send.code.via.sms=Envoyer le code par SMS
+send.email.otp=Envoyer un OTP par e-mail
+send.code.via.sms=Envoyer un OTP par SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -303,8 +309,10 @@ enter.your.email=Entrer votre Email
 enter.your.username=Entrez votre nom d'utilisateur
 error.enter.code=Veuillez entrer le code !
 choose.other.option=Choisissez une autre option
-resend.code.success=Un nouveau OTP a été envoyé sur votre mobile.
+resend.code.success=Un nouvel OTP a été envoyé sur votre mobile.
+resend.code.success.email=Un nouvel OTP a été envoyé à votre adresse e-mail.
 enter.code.sent.smsotp=Veuillez entrer le code envoyé sur votre téléphone portable
+enter.code.sent.emailotp=Entrez le code envoyé à votre email
 sms.otp.resend.code=Renvoyer le code
 sms.otp.submit.button=Continuer
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_ja_JP.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_ja_JP.properties
index 638602f313a..a4943c9124d 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_ja_JP.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_ja_JP.properties
@@ -34,14 +34,20 @@ sign.up.button=サインアップ
 sms.otp.heading=OTP認証
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=OTP認証
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=パスワード
 password.recovery.body=ご対応させていただきます。ユーザー名を入力してパスワードのリセットを開始します。
 password.recovery.identifier.input.placeholder=ユーザー名
 password.recovery.button.email.link=リセットリンクの送信
+password.recovery.button.email.otp=電子メール OTP を送信
 password.recovery.button.smsotp=SMSコードを送信する
 send.email.link=電子メールでコードを送信する
-send.code.via.sms=コードをSMSで送信する
+send.email.otp=OTPをメールで送信
+send.code.via.sms=OTPをSMSで送信
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -298,8 +304,10 @@ enter.your.email=メールアドレスを入力
 enter.your.username=ユーザー名を入力して下さい
 error.enter.code=コードを入力してください！
 choose.other.option=別のオプションを選択
-resend.code.success=新しいOTPがあなたの携帯電話に送信されました。
+resend.code.success=新しい OTP が携帯電話に送信されました。
+resend.code.success.email=新しい OTP があなたのメールに送信されました。
 enter.code.sent.smsotp=携帯電話に送信されたコードを入力してください
+enter.code.sent.emailotp=メールで送信されたコードを入力してください
 sms.otp.resend.code=再送コード
 sms.otp.submit.button=続行します
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_BR.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_BR.properties
index fda6f6c19a2..7d887ef8dc3 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_BR.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_BR.properties
@@ -34,14 +34,20 @@ sign.up.button=Cadastrar
 sms.otp.heading=Verificação de OTP
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=Verificação de OTP
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=Esqueceu a senha?
 password.recovery.body=Estamos protegendo você. Digite seu nome de usuário para começar a redefinir sua senha.
 password.recovery.identifier.input.placeholder=Nome de usuário
 password.recovery.button.email.link=Enviar link de redefinição
+password.recovery.button.email.otp=Enviar e-mail OTP
 password.recovery.button.smsotp=enviar codigo sms
 send.email.link=Enviar link de redefinição por e-mail
-send.code.via.sms=Enviar código por SMS
+send.email.otp=Enviar OTP por e-mail
+send.code.via.sms=Enviar OTP via SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -302,8 +308,10 @@ enter.your.email=Digite seu e-mail
 enter.your.username=Digite seu nome de usuário
 error.enter.code=Por favor, digite o código!
 choose.other.option=Escolha uma opção diferente
-resend.code.success=Um novo OTP foi enviado para o seu celular.
+resend.code.success=Uma nova OTP foi enviada para o seu telemóvel.
+resend.code.success.email=Um novo OTP foi enviado para seu e-mail.
 enter.code.sent.smsotp=Digite o código enviado para o seu celular
+enter.code.sent.emailotp=Digite o código enviado para seu e-mail
 sms.otp.resend.code=Reenviar código
 sms.otp.submit.button=Continuar
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_PT.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_PT.properties
index a985948c4af..3bc67e8f3d6 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_PT.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_pt_PT.properties
@@ -34,14 +34,20 @@ sign.up.button=Inscrever-se
 sms.otp.heading=Verificação de OTP
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=Verificação de OTP
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=Esqueceu a senha?
 password.recovery.body=Estamos protegendo você. Digite seu nome de usuário para começar a redefinir sua senha.
 password.recovery.identifier.input.placeholder=Nome de usuário
 password.recovery.button.email.link=Enviar link de redefinição
+password.recovery.button.email.otp=Enviar e-mail OTP
 password.recovery.button.smsotp=Enviar codigo sms
 send.email.link=Enviar link de redefinição por e-mail
-send.code.via.sms=Enviar código por SMS
+send.email.otp=Enviar OTP por e-mail
+send.code.via.sms=Enviar OTP via SMS
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -319,8 +325,10 @@ enter.your.email=Digite seu e-mail
 enter.your.username=Entre com seu nome de usuário
 error.enter.code=Por favor, digite o código!
 choose.other.option=Escolha uma opção diferente
-resend.code.success=Um novo OTP foi enviado para o seu telemóvel.
+resend.code.success=Uma nova OTP foi enviada para o seu telemóvel.
+resend.code.success.email=Um novo OTP foi enviado para seu e-mail.
 enter.code.sent.smsotp=Insira o código enviado para o seu telemóvel
+enter.code.sent.emailotp=Digite o código enviado para seu e-mail
 sms.otp.resend.code=Reenviar código
 sms.otp.submit.button=Continuar
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_zh_CN.properties b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_zh_CN.properties
index 6dd87e8f567..26115a6ceee 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_zh_CN.properties
+++ b/identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources_zh_CN.properties
@@ -34,14 +34,20 @@ sign.up.button=报名
 sms.otp.heading=OTP验证
 # <!-- End of SMS OTP Screen Translations -->
 
+# <!-- Start of Email OTP Screen Translations -->
+email.otp.heading=OTP验证
+# <!-- End of Email OTP Screen Translations -->
+
 # <!-- Start of Password Recovery Translations -->
 password.recovery.heading=密码
 password.recovery.body=我们已经为您提供了保障。输入您的用户名以开始重置密码。
 password.recovery.identifier.input.placeholder=用户名
 password.recovery.button.email.link=发送重置链接
+password.recovery.button.email.otp=发送电子邮件一次性密码
 password.recovery.button.smsotp=傳簡訊代碼
 send.email.link=透過電子郵件發送重置鏈接
-send.code.via.sms=透過簡訊發送代碼
+send.email.otp=通过电子邮件发送 OTP
+send.code.via.sms=通过短信发送 OTP
 # <!-- End of Password Recovery Translations -->
 
 # <!-- Start of Password Reset Translations -->
@@ -299,8 +305,10 @@ enter.your.email=输入你的电子邮箱
 enter.your.username=输入你的用户名
 error.enter.code=请输入代码
 choose.other.option=选择其他选项
-resend.code.success=新的OTP已发送到您的手机。
+resend.code.success=新的 OTP 已發送至您的手機。
+resend.code.success.email=新的 OTP 已发送至您的电子邮箱
 enter.code.sent.smsotp=输入发送到您手机上的代码
+enter.code.sent.emailotp=输入发送到您电子邮箱的代码
 sms.otp.resend.code=重新发送验证码
 sms.otp.submit.button=继续
 
diff --git a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp
index 29badc4e3da..93934e1d706 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp
+++ b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp
@@ -46,6 +46,7 @@
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.PreferenceRetrievalClient" %>
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.PreferenceRetrievalClientException" %>
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointConstants" %>
+<%@ page import="org.wso2.carbon.user.core.util.UserCoreUtil" %>
 <%@ page import="org.owasp.encoder.Encode" %>
 
 <%-- Include tenant context --%>
@@ -150,11 +151,15 @@
     String recoveryStage = request.getParameter("recoveryStage");
 
     if (RecoveryStage.INITIATE.equalsValue(recoveryStage)) {
-        // if otp is supported by a new channel (eg: email) update this value assignment. null means unsupported.
-        final String targetChannel = IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP
-            .equals((String)request.getAttribute("channel"))
+        // If otp is supported by a new channel update this value assignment. null means unsupported.
+        final String targetChannel =IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP
+            .equals((String) request.getAttribute("channel")) 
                 ? "SMS"
-                : null;
+                : IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL
+                .equals((String) request.getAttribute("channel"))
+                    ? "EMAIL"
+                    : null;
+
         // Manage unsupported channel
         if (StringUtils.isBlank(targetChannel)) {
             redirectToErrorPageWithMessage(request, response, "Unknown.channel");
@@ -186,10 +191,11 @@
             if (resp == null) {
                 /** Handle invalid username scenario. proceeds to next level without warning to 
                 avoid an attacker bruteforcing to learn the usernames. */
+                
                 request.setAttribute("screenValue", "******" + getRandomNumberString(4, username));
                 request.setAttribute("resendCode", UUID.randomUUID().toString());
                 request.setAttribute("flowConfirmationCode", UUID.randomUUID().toString());
-                request.getRequestDispatcher("sms-otp.jsp").forward(request, response);
+                request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
                 return;
             }
             for(AccountRecoveryType recoveryType: resp) {
@@ -238,7 +244,7 @@
             return;
         }
         // Redirect to enter the OTP.
-        request.getRequestDispatcher("sms-otp.jsp").forward(request, response);
+        request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
     } else if (RecoveryStage.RESEND.equalsValue(recoveryStage)) {
         String resendCode = request.getParameter("resendCode");
         String flowConfirmationCode = request.getParameter("flowConfirmationCode");
@@ -272,7 +278,7 @@
         request.setAttribute("resendCode", resendCode);
         request.setAttribute("sp", request.getParameter("sp"));
         request.setAttribute("flowConfirmationCode", flowConfirmationCode);
-        request.getRequestDispatcher("sms-otp.jsp").forward(request, response);
+        request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
     } else if (RecoveryStage.CONFIRM.equalsValue(recoveryStage)) {
         String flowConfirmationCode = request.getParameter("flowConfirmationCode"); 
         String OTPcode = request.getParameter("OTPcode");
@@ -301,7 +307,7 @@
             request.setAttribute("resendCode", request.getParameter("resendCode"));
             request.setAttribute("sp", request.getParameter("sp"));
             request.setAttribute("flowConfirmationCode", flowConfirmationCode);
-            request.getRequestDispatcher("sms-otp.jsp").forward(request, response);
+            request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
             return;
         }
         String spId = Encode.forJava(request.getParameter("spId"));
diff --git a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-with-claims-options.jsp b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-with-claims-options.jsp
index 4d25e04c60f..81672ce5596 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-with-claims-options.jsp
+++ b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-with-claims-options.jsp
@@ -30,6 +30,8 @@
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.model.recovery.v2.*" %>
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.api.UsernameRecoveryApi" %>
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.model.*" %>
+<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.PreferenceRetrievalClient" %>
+<%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.client.PreferenceRetrievalClientException" %>
 <%@ page import="org.wso2.carbon.identity.mgt.endpoint.util.IdentityManagementEndpointConstants" %>
 <%@ page import="java.net.URISyntaxException" %>
 <%@ page import="java.io.File" %>
@@ -59,12 +61,29 @@
     boolean isChallengeQuestionsEnabled = false;
 
     boolean isEmailEnabled = false;
+    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = false;
     boolean isSMSOTPEnabled = false;
     String recoveryCode = "";
     String emailId = "";
     String smsOtpId = "";
     String screenValue = "";
 
+    try {
+        PreferenceRetrievalClient preferenceRetrievalClient = new PreferenceRetrievalClient();
+        isEmailOtpBasedPasswordRecoveryEnabledByTenant = 
+            preferenceRetrievalClient.checkEmailOTPBasedPasswordRecovery(tenantDomain);
+    } catch (PreferenceRetrievalClientException e) {
+        request.setAttribute("error", true);
+        request.setAttribute("errorMsg", IdentityManagementEndpointUtil
+                        .i18n(recoveryResourceBundle, "something.went.wrong.contact.admin"));
+        IdentityManagementEndpointUtil.addErrorInformation(request, e);
+        if (!StringUtils.isBlank(username)) {
+            request.setAttribute("username", username);
+        }
+        request.getRequestDispatcher("error.jsp").forward(request, response);
+        return;
+    }
+ 
     List<Claim> claims;
     UsernameRecoveryApi usernameRecoveryApi = new UsernameRecoveryApi();
     try {
@@ -230,12 +249,16 @@
                     <form class="ui large form" method="post" action="verify.do" id="recoverDetailsForm">
                         <div class="segment" style="text-align: left;">
                         <% if (isNotificationBasedRecoveryEnabled) { %>
-                        <% if (isEmailEnabled) { %>
+                        <% if (isEmailEnabled) { 
+                            String emailSendLabel = isEmailOtpBasedPasswordRecoveryEnabledByTenant
+                                                        ? "send.email.otp" 
+                                                        : "send.email.link";
+                        %>
                             <div class="field">
                                 <div class="ui radio checkbox">
                                     <input type="radio" name="recoveryOption" value="<%=emailId%>" checked/>
                                     <label><%=IdentityManagementEndpointUtil.i18n
-                                            (recoveryResourceBundle,"send.email.link")%></label>
+                                            (recoveryResourceBundle, emailSendLabel)%></label>
                                 </div>
                             </div>
                         <%
@@ -284,6 +307,9 @@
                                 value="<%=Encode.forHtmlAttribute(tenantDomain) %>"/>
                             <input type="hidden" name="recoveryCode" value="<%=recoveryCode %>"/>
                             <input type="hidden" name="isPasswordRecoveryWithClaimsNotify" value="true">
+                            <input type="hidden" name="isEmailOtpBasedPasswordRecoveryEnabledByTenant"
+                                value="<%=isEmailOtpBasedPasswordRecoveryEnabledByTenant %>"/>
+                            <input type="hidden" name="recoveryStage" value="INITIATE"/>
                         </div>
                         <div class="ui divider hidden"></div>
                         <div class="align-right buttons">
diff --git a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
index 38a1a2d62ad..7c56bfd5558 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
+++ b/identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
@@ -145,6 +145,7 @@
     Boolean isQuestionBasedPasswordRecoveryEnabledByTenant = false;
     Boolean isSMSRecoveryAvailable = false;
     Boolean isEmailLinkBasedPasswordRecoveryEnabledByTenant = false;
+    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = false;
     Boolean isMultiAttributeLoginEnabledInTenant = false;
     String allowedAttributes = null;
     try {
@@ -154,6 +155,8 @@
             Boolean.parseBoolean(IdentityUtil.getProperty("Connectors.ChallengeQuestions.Enabled"));
         isEmailLinkBasedPasswordRecoveryEnabledByTenant =
             preferenceRetrievalClient.checkEmailLinkBasedPasswordRecovery(tenantDomain);
+        isEmailOtpBasedPasswordRecoveryEnabledByTenant = 
+            preferenceRetrievalClient.checkEmailOTPBasedPasswordRecovery(tenantDomain);
         isSMSRecoveryAvailable = preferenceRetrievalClient.checkSMSOTPBasedPasswordRecovery(tenantDomain);
         isMultiAttributeLoginEnabledInTenant = preferenceRetrievalClient.checkMultiAttributeLogin(tenantDomain);
         allowedAttributes = preferenceRetrievalClient.checkMultiAttributeLoginProperty(tenantDomain);
@@ -168,8 +171,8 @@
         request.getRequestDispatcher("error.jsp").forward(request, response);
         return;
     }
-    Boolean isEmailRecoveryAvailable = isEmailNotificationEnabled && isEmailLinkBasedPasswordRecoveryEnabledByTenant;
-
+    Boolean isEmailRecoveryAvailable = isEmailNotificationEnabled && 
+        (isEmailLinkBasedPasswordRecoveryEnabledByTenant || isEmailOtpBasedPasswordRecoveryEnabledByTenant);
     String emailUsernameEnable = application.getInitParameter("EnableEmailUserName");
     Boolean isEmailUsernameEnabled = false;
     String usernameLabel = "Username";
@@ -350,12 +353,15 @@
                             data-testid="password-recovery-page-multi-option-radio">
                             <%
                                 if (isEmailRecoveryAvailable) {
+                                    String emailSendLabel = isEmailOtpBasedPasswordRecoveryEnabledByTenant
+                                                                ? "send.email.otp" 
+                                                                : "send.email.link";
                             %>
                             <div class="field">
                                 <div class="ui radio checkbox">
                                     <input type="radio" name="recoveryOption" value="<%=EMAIL%>"
                                         <%=EMAIL.equals(selectedOption)?"checked":""%>/>
-                                    <label><%=i18n(recoveryResourceBundle, customText, "send.email.link")%>
+                                    <label><%=i18n(recoveryResourceBundle, customText, emailSendLabel)%>
                                     </label>
                                 </div>
                             </div>
@@ -403,6 +409,8 @@
                         <input type="hidden" name="urlQuery" value="<%=request.getQueryString() %>"/>
                         <input type="hidden" name="isMultiRecoveryOptionsAvailable"
                             value="<%=multipleRecoveryOptionsAvailable %>"/>
+                        <input type="hidden" name="isEmailOtpBasedPasswordRecoveryEnabledByTenant"
+                            value="<%=isEmailOtpBasedPasswordRecoveryEnabledByTenant %>"/>
 
                         <%
                             String callback = request.getParameter("callback");
@@ -443,11 +451,14 @@
                         <div class="mt-4">
                             <%
                                 String submitButtoni18nText =
-                                    multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
-                                    ( isQuestionBasedPasswordRecoveryEnabledByTenant? "Recover.with.question" :
+                                    multipleRecoveryOptionsAvailable ? "password.recovery.button.multi" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant ?
+                                        "password.recovery.button.email.otp" :
+                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant ? 
+                                        "password.recovery.button.email.link" :
+                                    ( isQuestionBasedPasswordRecoveryEnabledByTenant ? "Recover.with.question" :
                                     ( isSMSRecoveryAvailable ? "password.recovery.button.smsotp" :
-                                    "Submit")));
+                                    "Submit"))));
                             %>
                             <button id="recoverySubmit"
                                     class="ui primary button large fluid"
diff --git a/identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp b/identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
index b65d5b0b560..225b0e2428c 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
+++ b/identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
@@ -359,6 +359,15 @@
                 notificationChannel = recoveryResponse.getNotificationChannel();
                 request.setAttribute("callback", callback);
                 if (notificationChannel.equals("EMAIL")) {
+                    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                        Encode.forJava(request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant")));
+                    if (isEmailOtpBasedPasswordRecoveryEnabledByTenant) {
+                        request.setAttribute("channel", IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL);
+                        request.setAttribute("resendCode", recoveryResponse.getResendCode());
+                        request.setAttribute("flowConfirmationCode", recoveryResponse.getFlowConfirmationCode());
+                        request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
+                        return;
+                    }
                     request.getRequestDispatcher("password-recovery-with-claims-notify.jsp").forward(request,
                             response);
                     return;
@@ -366,7 +375,7 @@
                     request.setAttribute("screenValue", request.getParameter("screenValue"));
                     request.setAttribute("resendCode", recoveryResponse.getResendCode());
                     request.setAttribute("flowConfirmationCode", recoveryResponse.getFlowConfirmationCode());
-                    request.getRequestDispatcher("sms-otp.jsp").forward(request, response);
+                    request.getRequestDispatcher("sms-and-email-otp.jsp").forward(request, response);
                 } else {
                     request.setAttribute("error", true);
                     request.setAttribute("errorMsg", IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,
@@ -399,6 +408,13 @@
             session.setAttribute("username", username);
 
             if (IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL.equals(recoveryOption)) {
+                Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                    request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant"));
+                if (isEmailOtpBasedPasswordRecoveryEnabledByTenant) {
+                    request.setAttribute("channel", IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL);
+                    request.getRequestDispatcher("password-recovery-otp.jsp").forward(request, response);
+                    return;
+                }
                 request.setAttribute("callback", callback);
                 request.getRequestDispatcher("password-recovery-notify.jsp").forward(request, response);
             } else if(IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP.equals(recoveryOption)) {
diff --git a/identity-apps-core/apps/recovery-portal/src/main/webapp/sms-otp.jsp b/identity-apps-core/apps/recovery-portal/src/main/webapp/sms-and-email-otp.jsp
similarity index 94%
rename from identity-apps-core/apps/recovery-portal/src/main/webapp/sms-otp.jsp
rename to identity-apps-core/apps/recovery-portal/src/main/webapp/sms-and-email-otp.jsp
index 2fa552d9e91..a9e35a6fabc 100644
--- a/identity-apps-core/apps/recovery-portal/src/main/webapp/sms-otp.jsp
+++ b/identity-apps-core/apps/recovery-portal/src/main/webapp/sms-and-email-otp.jsp
@@ -40,8 +40,9 @@
 <%@ include file="includes/init-url.jsp" %>
 
 <%
-    // Add the sms-otp screen to the list to retrieve text branding customizations.
+    // Add the sms-otp and email-otp screen to the list to retrieve text branding customizations.
     screenNames.add("sms-otp");
+    screenNames.add("email-otp");
 %>
 
 <%-- Branding Preferences --%>
@@ -85,6 +86,11 @@
     if (screenValue == null) {
         screenValue = (String) request.getAttribute("screenValue");
     }
+    String channel = (String) request.getAttribute("channel");
+    if (channel == null) {
+        channel = Encode.forJava(request.getParameter("channel"));
+    }
+    boolean isEmailOtp = IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL.equals(channel);
     String errorMessage = IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "error");
     boolean authenticationFailed = Boolean.parseBoolean((String)request.getAttribute("isAuthFailure"));
     boolean resendFailed = Boolean.parseBoolean((String)request.getAttribute("isResendFailure"));
@@ -153,7 +159,8 @@
             <div class="ui segment">
                     <%-- page content --%>
                     <h2>
-                    <%= i18n(recoveryResourceBundle, customText, "sms.otp.heading") %>
+                    <%= i18n(recoveryResourceBundle, customText,
+                            isEmailOtp ? "email.otp.heading" : "sms.otp.heading") %>
                     </h2>
                     <div class="ui divider hidden"></div>
                     <%
@@ -165,9 +172,11 @@
                     <% } %>
                     <%
                         if ("true".equals(String.valueOf((Object)request.getAttribute("resendSuccess")))) {
+                            String resendSuccessMessage = isEmailOtp ? "resend.code.success.email"
+                                                                     : "resend.code.success";
                     %>
                     <div id="resend-msg" class="ui positive message">
-                        <%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, "resend.code.success")%>
+                        <%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle, resendSuccessMessage)%>
                     </div>
                     <%
                         }
@@ -190,17 +199,19 @@
                             } %>
 
                             <div class="field">
-                                <% if (screenValue != null) { %>
+                                <% String otpHeader = isEmailOtp ? "enter.code.sent.emailotp" : "enter.code.sent.smsotp";
+
+                                if (screenValue != null && !isEmailOtp) { %>
                                     <input type='hidden' name='screenValue' id='screenValue'
                                         value='<%=Encode.forHtmlContent(screenValue)%>'/>
-                                    <label for="password">
+                                    <label for="password" class="text-center">
                                         <%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,
-                                            "enter.code.sent.smsotp")%> (<%=Encode.forHtmlContent(screenValue)%>)
+                                            otpHeader)%> (<%=Encode.forHtmlContent(screenValue)%>)
                                     </label>
                                 <% } else { %>
-                                    <label for="password">
+                                    <label for="password" class="text-center">
                                         <%=IdentityManagementEndpointUtil.i18n(recoveryResourceBundle,
-                                            "enter.code.sent.smsotp")%>:
+                                           otpHeader)%>
                                     </label>
                                 <% } %>
 
@@ -261,7 +272,8 @@
                             <input type='hidden' name='username'
                                 value='<%=Encode.forHtmlAttribute(username)%>'/>
                             <input type="hidden" name="channel"
-                                value='<%=IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP%>'/>
+                                value='<%=isEmailOtp ? IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL
+                                        : IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP%>'/>
                             <input type="hidden" id="recoveryStage" name="recoveryStage"
                                 value='CONFIRM'/>
                             <input type="hidden" name="resendCode"
@@ -345,12 +357,13 @@
                                         .addPath("/accountrecoveryendpoint/recoveraccountrouter.do").build()
                                         .getRelativePublicURL();
                                     String multiOptionPathWithQuery;
+                                    String selectedOption = isEmailOtp ? "EMAIL" : "SMSOTP";
                                     if (urlQuery.contains("&username=")) {
                                         multiOptionPathWithQuery =
                                             urlQuery.replaceAll("(&username=)[^&]+", "$1" + username);
                                     } else {
                                         multiOptionPathWithQuery = urlQuery + "&username=" + username
-                                            + "&selectedOption=SMSOTP";
+                                            + "&selectedOption=" + selectedOption;
                                     }
                                 %>
                                 <a class="ui primary basic button link-button" id="goBackLink"
diff --git a/modules/i18n/src/models/namespaces/branding-ns.ts b/modules/i18n/src/models/namespaces/branding-ns.ts
index 19ff346518f..5af7a342769 100644
--- a/modules/i18n/src/models/namespaces/branding-ns.ts
+++ b/modules/i18n/src/models/namespaces/branding-ns.ts
@@ -183,6 +183,7 @@ export interface BrandingNS {
     variations: {
         "sms-otp": string;
         "email-link": string;
+        "email-otp": string;
         "base": string;
         "multi": string;
         "sms": string;
diff --git a/modules/i18n/src/translations/en-US/portals/branding.ts b/modules/i18n/src/translations/en-US/portals/branding.ts
index 1bfce3ba798..263d89f86c5 100644
--- a/modules/i18n/src/translations/en-US/portals/branding.ts
+++ b/modules/i18n/src/translations/en-US/portals/branding.ts
@@ -182,6 +182,7 @@ export const branding: BrandingNS = {
     },
     variations: {
         "sms-otp": "SMS OTP",
+        "email-otp": "Email OTP",
         "email-link": "Email Link",
         "base": "Base",
         "multi": "Multi Option",


Reviews:
user: login: DonOmalVindula
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T06:44:00Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:09:47Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:09:59Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:10:09Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:10:22Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:10:39Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:10:48Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:10:57Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:11:06Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T08:11:17Z
user: login: JayaShakthi97
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T09:30:11Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T10:07:21Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:25:11Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:29:33Z
user: login: pavinduLakshan
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-20T10:10:20Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:12:35Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:14:40Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:29:00Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:29:35Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:15:20Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:17:18Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:17:24Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:17:38Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:17:50Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:18:09Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T04:59:27Z
user: login: pavinduLakshan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T02:34:17Z
user: login: pavinduLakshan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T02:38:20Z
user: login: pavinduLakshan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T02:40:48Z
user: login: pavinduLakshan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-22T02:41:59Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T04:34:40Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T04:36:16Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T04:36:27Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T04:36:31Z
user: login: Malith-19
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T17:27:32Z
user: login: JayaShakthi97
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T02:36:16Z
user: login: pavinduLakshan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T02:39:20Z

Review comments:
diff_hunk: @@ -3222,17 +3222,17 @@ export const extensions: Extensions = {
                                 label: "Enable SMS based recovery"
                             },
                             expiryTime: {
-                                hint: "Password recovery link expiry time in minutes.",
-                                label: "Recovery link expiry time",
+                                hint: "Password recovery link/OTP expiry time in minutes.",
+                                label: "Recovery link/OTP expiry time",
                                 placeholder: "Enter expiry time",
                                 validations: {
-                                    invalid: "Recovery link expiry time should be an integer.",
-                                    empty: "Recovery link expiry time cannot be empty.",
+                                    invalid: "Recovery link/OTP expiry time should be an integer.",
+                                    empty: "Recovery link/Otp expiry time cannot be empty.",
path: apps/console/src/extensions/i18n/resources/en-US/extensions.ts
user: login: DonOmalVindula
body: ```suggestion
                                    empty: "Recovery link/OTP expiry time cannot be empty.",
```
created_at: 2025-03-19T06:04:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,8 +38,12 @@
         div:nth-child(6),
         div:nth-child(7),
         div:nth-child(8),
-        div:nth-child(13),
-        div:nth-child(14) {
+        div:nth-child(9),
+        div:nth-child(10),
+        div:nth-child(11),
+        div:nth-child(12),
+        div:nth-child(17),
+        div:nth-child(18) {
         margin-left: 50px;
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss
user: login: DonOmalVindula
body: Do we have a better way of handling this CSS?
created_at: 2025-03-19T06:35:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,29 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: DonOmalVindula
body: ```suggestion
```
created_at: 2025-03-19T06:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,29 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
+                </Heading>
+                {
+                    EMAIL_RECOVERY_RADIO_OPTIONS.map((option: RadioChild) => (
+                        <Field.Radio
+                            key={ option.value }
+                            ariaLabel={ t(option.label) }
+                            label={ t(option.label) }
+                            name={ "emailRecoveryOption" }
+                            type="radio"
+                            value={ option.value }
+                            checked={ emailRecoveryOption === option.value }
+                            listen={ () => setEmailRecoveryOption(option.value) }
+                            disabled={ !isEmailRecoveryEnabled }
+                            readOnly={ readOnly }
+                        />
+                    ))
+                }
+                <br/>
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: DonOmalVindula
body: ```suggestion
```
created_at: 2025-03-19T06:37:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -374,6 +426,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     data-testid={ `${ testId }-notify-success` }
                     data-componentid={ `${ testId }-notify-success` }
                 />
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: DonOmalVindula
body: ```suggestion
```
created_at: 2025-03-19T06:37:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,9 +153,11 @@
     if (RecoveryStage.INITIATE.equalsValue(recoveryStage)) {
         // if otp is supported by a new channel (eg: email) update this value assignment. null means unsupported.
         final String targetChannel = IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP
-            .equals((String)request.getAttribute("channel"))
-                ? "SMS"
-                : null;
+            .equals((String) request.getAttribute("channel")) ? "SMS"  
+            : IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL
+            .equals((String) request.getAttribute("channel")) ? "EMAIL"
+            : null;
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp
user: login: DonOmalVindula
body: Fix indentation here
created_at: 2025-03-19T06:40:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: DonOmalVindula
body: ```suggestion
                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant ?
```
created_at: 2025-03-19T06:41:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
+                                        "password.recovery.button.email.otp" :
+                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant? 
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: DonOmalVindula
body: ```suggestion
                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant ? 
```
created_at: 2025-03-19T06:41:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
+                                        "password.recovery.button.email.otp" :
+                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant? 
+                                        "password.recovery.button.email.link" :
                                     ( isQuestionBasedPasswordRecoveryEnabledByTenant? "Recover.with.question" :
                                     ( isSMSRecoveryAvailable ? "password.recovery.button.smsotp" :
-                                    "Submit")));
+                                    "Submit"))));
                             %>
                             <button id="recoverySubmit"
                                     class="ui primary button large fluid"
                                     type="submit">
                                     <%=i18n(recoveryResourceBundle, customText, submitButtoni18nText)%>
                             </button>
+                            
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: DonOmalVindula
body: ```suggestion
```
created_at: 2025-03-19T06:42:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -397,8 +397,16 @@
         } else {
             request.setAttribute("username", username);
             session.setAttribute("username", username);
+            
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: DonOmalVindula
body: ```suggestion
```
created_at: 2025-03-19T06:42:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -397,8 +397,16 @@
         } else {
             request.setAttribute("username", username);
             session.setAttribute("username", username);
+            
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:09:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
+                                        "password.recovery.button.email.otp" :
+                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant? 
+                                        "password.recovery.button.email.link" :
                                     ( isQuestionBasedPasswordRecoveryEnabledByTenant? "Recover.with.question" :
                                     ( isSMSRecoveryAvailable ? "password.recovery.button.smsotp" :
-                                    "Submit")));
+                                    "Submit"))));
                             %>
                             <button id="recoverySubmit"
                                     class="ui primary button large fluid"
                                     type="submit">
                                     <%=i18n(recoveryResourceBundle, customText, submitButtoni18nText)%>
                             </button>
+                            
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:09:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
+                                        "password.recovery.button.email.otp" :
+                                    ( isEmailLinkBasedPasswordRecoveryEnabledByTenant? 
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:10:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -444,16 +452,20 @@
                             <%
                                 String submitButtoni18nText =
                                     multipleRecoveryOptionsAvailable? "password.recovery.button.multi" :
-                                    ( isEmailRecoveryAvailable? "password.recovery.button.email.link" :
+                                    ( isEmailOtpBasedPasswordRecoveryEnabledByTenant?
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:10:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -152,9 +153,11 @@
     if (RecoveryStage.INITIATE.equalsValue(recoveryStage)) {
         // if otp is supported by a new channel (eg: email) update this value assignment. null means unsupported.
         final String targetChannel = IdentityManagementEndpointConstants.PasswordRecoveryOptions.SMSOTP
-            .equals((String)request.getAttribute("channel"))
-                ? "SMS"
-                : null;
+            .equals((String) request.getAttribute("channel")) ? "SMS"  
+            : IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL
+            .equals((String) request.getAttribute("channel")) ? "EMAIL"
+            : null;
path: identity-apps-core/apps/recovery-portal/src/main/webapp/password-recovery-otp.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:10:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -374,6 +426,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     data-testid={ `${ testId }-notify-success` }
                     data-componentid={ `${ testId }-notify-success` }
                 />
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:10:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,29 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
+                </Heading>
+                {
+                    EMAIL_RECOVERY_RADIO_OPTIONS.map((option: RadioChild) => (
+                        <Field.Radio
+                            key={ option.value }
+                            ariaLabel={ t(option.label) }
+                            label={ t(option.label) }
+                            name={ "emailRecoveryOption" }
+                            type="radio"
+                            value={ option.value }
+                            checked={ emailRecoveryOption === option.value }
+                            listen={ () => setEmailRecoveryOption(option.value) }
+                            disabled={ !isEmailRecoveryEnabled }
+                            readOnly={ readOnly }
+                        />
+                    ))
+                }
+                <br/>
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:10:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,29 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:11:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3222,17 +3222,17 @@ export const extensions: Extensions = {
                                 label: "Enable SMS based recovery"
                             },
                             expiryTime: {
-                                hint: "Password recovery link expiry time in minutes.",
-                                label: "Recovery link expiry time",
+                                hint: "Password recovery link/OTP expiry time in minutes.",
+                                label: "Recovery link/OTP expiry time",
                                 placeholder: "Enter expiry time",
                                 validations: {
-                                    invalid: "Recovery link expiry time should be an integer.",
-                                    empty: "Recovery link expiry time cannot be empty.",
+                                    invalid: "Recovery link/OTP expiry time should be an integer.",
+                                    empty: "Recovery link/Otp expiry time cannot be empty.",
path: apps/console/src/extensions/i18n/resources/en-US/extensions.ts
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/fbbbdb324303b4579a9429db2ac3c9415b01112a
created_at: 2025-03-19T08:11:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2025, 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 { IdentifiableComponentInterface } from "@wso2is/core/models";
+import React, { FunctionComponent, ReactElement } from "react";
+import { CustomTextPreferenceConstants } from "../../../../../constants/custom-text-preference-constants";
+import useBrandingPreference from "../../../../../hooks/use-branding-preference";
+
+/**
+ * Proptypes for the password-recovery fragment of login screen skeleton.
+ */
+export type PasswordRecoveryEmailOTPFragmentInterface = IdentifiableComponentInterface;
+
+/**
+ * Password recovery fragment component for the branding preview of Password Recovery box.
+ *
+ * @param props - Props injected to the component.
+ * @returns Password recovery fragment component.
+ */
+const PasswordRecoveryEmailOTPFragment: FunctionComponent<PasswordRecoveryEmailOTPFragmentInterface> = (
+    props: PasswordRecoveryEmailOTPFragmentInterface
+): ReactElement => {
+    const { ["data-componentid"]: componentId } = props;
+
+    const { i18n } = useBrandingPreference();
+
+    return (
+        <div data-componentid={ componentId }>
+            <h2>
+                { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.HEADING, "Forgot Password?") }
+            </h2>
+            <div className="segment-form">
+                <form method="post" id="otpForm" className="ui large form otp-form">
+                    <p className="line-break text-center" id="instruction" >
+                        { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BODY,
+                            "Don't worry, it happens. We will send you an email to reset your password.") }
+                    </p>
+                    <div className="ui divider hidden"></div>
+                    <div className="ui large form">
+                        <div className="field">
+                            <div className="ui fluid left icon input">
+                                <input
+                                    type="text"
+                                    id="usernameUserInput"
+                                    name="usernameUserInput"
+                                    placeholder={
+                                        i18n(CustomTextPreferenceConstants
+                                            .TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY
+                                            .IDENTIFIER.INPUT.PLACEHOLDER, "Username")
+                                    }
+                                    data-componentid="login-page-username-input"
+                                />
+                                <i aria-hidden="true" className="user outline icon"></i>
+                                <input id="username" name="username" type="hidden" />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="ui divider hidden"></div>
+                    <div>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui primary fluid large button mb-2"
+                        >
+                            { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BUTTON.EMAIL_OTP,
+                                "Send Email OTP") }
+                        </button>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui secondary fluid large button"
+                        >
+                            Cancel
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    );
+};
+
+/**
+ * Default props for the component.
+ */
+PasswordRecoveryEmailOTPFragment.defaultProps = {
path: features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-email-otp-fragment.tsx
user: login: JayaShakthi97
body: Do not use `defaultProps` as it is deprecated now.
Refer: https://react.dev/blog/2024/04/25/react-19-upgrade-guide#removed-proptypes-and-defaultprops
created_at: 2025-03-19T09:23:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2025, 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 { IdentifiableComponentInterface } from "@wso2is/core/models";
+import React, { FunctionComponent, ReactElement } from "react";
+import { CustomTextPreferenceConstants } from "../../../../../constants/custom-text-preference-constants";
+import useBrandingPreference from "../../../../../hooks/use-branding-preference";
+
+/**
+ * Proptypes for the password-recovery fragment of login screen skeleton.
+ */
+export type PasswordRecoveryEmailOTPFragmentInterface = IdentifiableComponentInterface;
+
+/**
+ * Password recovery fragment component for the branding preview of Password Recovery box.
+ *
+ * @param props - Props injected to the component.
+ * @returns Password recovery fragment component.
+ */
+const PasswordRecoveryEmailOTPFragment: FunctionComponent<PasswordRecoveryEmailOTPFragmentInterface> = (
+    props: PasswordRecoveryEmailOTPFragmentInterface
+): ReactElement => {
+    const { ["data-componentid"]: componentId } = props;
+
+    const { i18n } = useBrandingPreference();
+
+    return (
+        <div data-componentid={ componentId }>
+            <h2>
+                { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.HEADING, "Forgot Password?") }
+            </h2>
+            <div className="segment-form">
+                <form method="post" id="otpForm" className="ui large form otp-form">
+                    <p className="line-break text-center" id="instruction" >
+                        { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BODY,
+                            "Don't worry, it happens. We will send you an email to reset your password.") }
+                    </p>
+                    <div className="ui divider hidden"></div>
+                    <div className="ui large form">
+                        <div className="field">
+                            <div className="ui fluid left icon input">
+                                <input
+                                    type="text"
+                                    id="usernameUserInput"
+                                    name="usernameUserInput"
+                                    placeholder={
+                                        i18n(CustomTextPreferenceConstants
+                                            .TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY
+                                            .IDENTIFIER.INPUT.PLACEHOLDER, "Username")
+                                    }
+                                    data-componentid="login-page-username-input"
+                                />
+                                <i aria-hidden="true" className="user outline icon"></i>
+                                <input id="username" name="username" type="hidden" />
+                            </div>
+                        </div>
+                    </div>
+                    <div className="ui divider hidden"></div>
+                    <div>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui primary fluid large button mb-2"
+                        >
+                            { i18n(CustomTextPreferenceConstants.TEXT_BUNDLE_KEYS.PASSWORD_RECOVERY.BUTTON.EMAIL_OTP,
+                                "Send Email OTP") }
+                        </button>
+                        <button
+                            type="submit"
+                            id="subButton"
+                            className="ui secondary fluid large button"
+                        >
+                            Cancel
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    );
+};
+
+/**
+ * Default props for the component.
+ */
+PasswordRecoveryEmailOTPFragment.defaultProps = {
path: features/admin.branding.v1/components/preview/sign-in-box/fragments/password-recovery/password-recovery-email-otp-fragment.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/828ba0170ac7831d2a000b6a46be50d66347cde0
created_at: 2025-03-19T10:07:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,6 +382,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     readOnly={ readOnly }
                     width={ 10 }
                     disabled={ !isConnectorEnabled }
+                    defaultValue = { isEmailRecoveryEnabled }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: remove the additional spaces.
created_at: 2025-03-20T09:25:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -153,7 +156,8 @@
             <div class="ui segment">
                     <%-- page content --%>
                     <h2>
-                    <%= i18n(recoveryResourceBundle, customText, "sms.otp.heading") %>
+                    <%= i18n(recoveryResourceBundle, customText,
+                            isEmailOtp ? "email.otp.heading" : "sms.otp.heading") %>
path: identity-apps-core/apps/recovery-portal/src/main/webapp/otp.jsp
user: login: Malith-19
body: Center the text and remove the colon
created_at: 2025-03-20T09:29:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+---
+"@wso2is/admin.server-configurations.v1": patch
+"@wso2is/common.branding.v1": patch
+"@wso2is/admin.branding.v1": patch
+"@wso2is/identity-apps-core": patch
+"@wso2is/console": patch
path: .changeset/forty-coins-cross.md
user: login: pavinduLakshan
body: Since this is new feature, this should be minor
created_at: 2025-03-20T09:58:37Z
author_association: MEMBER
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: pavinduLakshan
body: WDYT?

```suggestion
resend.code.success.email=A new OTP was sent to your email.
```
created_at: 2025-03-20T10:06:08Z
author_association: MEMBER
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
 enter.code.sent.smsotp=Enter the code sent to your mobile phone
+enter.code.sent.emailotp=Enter the code sent to your email
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: pavinduLakshan
body: Is using the term `code` intentional? seems we have used the term `OTP` in all other places.
created_at: 2025-03-20T10:06:50Z
author_association: MEMBER
diff_hunk: @@ -362,6 +391,27 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: pavinduLakshan
body: we may not need this casting to ReactNode.
created_at: 2025-03-20T10:07:38Z
author_association: MEMBER
diff_hunk: @@ -362,6 +391,27 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
+                </Heading>
+                {
+                    EMAIL_RECOVERY_RADIO_OPTIONS.map((option: RadioChild) => (
+                        <Field.Radio
+                            key={ option.value }
+                            ariaLabel={ t(option.label) }
+                            label={ t(option.label) }
+                            name={ "emailRecoveryOption" }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: pavinduLakshan
body: ```suggestion
                            name="emailRecoveryOption"
```
created_at: 2025-03-20T10:07:53Z
author_association: MEMBER
diff_hunk: @@ -72,6 +74,18 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailLink.label",
+            value: EmailRecoveryOption.EMAIL_LINK },
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: pavinduLakshan
body: ```suggestion
            value: EmailRecoveryOption.EMAIL_LINK
        },
```
created_at: 2025-03-20T10:09:11Z
author_association: MEMBER
diff_hunk: @@ -72,6 +74,18 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailLink.label",
+            value: EmailRecoveryOption.EMAIL_LINK },
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailOtp.label",
+            value: EmailRecoveryOption.EMAIL_OTP }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: pavinduLakshan
body: ```suggestion
            value: EmailRecoveryOption.EMAIL_OTP
        }
```
created_at: 2025-03-20T10:09:20Z
author_association: MEMBER
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
 enter.code.sent.smsotp=Enter the code sent to your mobile phone
+enter.code.sent.emailotp=Enter the code sent to your email
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: Malith-19
body: To align with the smsotp i18n string(enter.code.sent.smsotp) I have followed the same.
created_at: 2025-03-20T10:12:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+---
+"@wso2is/admin.server-configurations.v1": patch
+"@wso2is/common.branding.v1": patch
+"@wso2is/admin.branding.v1": patch
+"@wso2is/identity-apps-core": patch
+"@wso2is/console": patch
path: .changeset/forty-coins-cross.md
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/5b860f17d38841a3ef785b02a960258e36d70a7c
created_at: 2025-03-20T10:14:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: Malith-19
body: we can update messages for both email and sms as `A new OTP has been sent to your email.`
created_at: 2025-03-20T10:29:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
 enter.code.sent.smsotp=Enter the code sent to your mobile phone
+enter.code.sent.emailotp=Enter the code sent to your email
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: Malith-19
body: Resolving as this has been used in the smsotp.
created_at: 2025-03-20T10:29:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -342,7 +351,9 @@ enter.your.username=Enter your username
 error.enter.code=Please enter the code!
 choose.other.option=Choose a different option
 resend.code.success=A new OTP is sent to your mobile.
+resend.code.success.email=A new OTP is sent to your email.
path: identity-apps-core/apps/recovery-portal/src/main/resources/org/wso2/carbon/identity/mgt/recovery/endpoint/i18n/Resources.properties
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/2b4b742cfd01c6a69d22d41ad72820e1f270a146
created_at: 2025-03-20T11:15:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +74,18 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailLink.label",
+            value: EmailRecoveryOption.EMAIL_LINK },
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailOtp.label",
+            value: EmailRecoveryOption.EMAIL_OTP }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/34e2e917aae565555d85c2f661b5e19633d56c20
created_at: 2025-03-20T11:17:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +74,18 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
+        {
+            label: "extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.emailLink.label",
+            value: EmailRecoveryOption.EMAIL_LINK },
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/34e2e917aae565555d85c2f661b5e19633d56c20
created_at: 2025-03-20T11:17:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,27 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
+                </Heading>
+                {
+                    EMAIL_RECOVERY_RADIO_OPTIONS.map((option: RadioChild) => (
+                        <Field.Radio
+                            key={ option.value }
+                            ariaLabel={ t(option.label) }
+                            label={ t(option.label) }
+                            name={ "emailRecoveryOption" }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/34e2e917aae565555d85c2f661b5e19633d56c20
created_at: 2025-03-20T11:17:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -362,6 +391,27 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     { t("extensions:manage.serverConfigurations.accountRecovery." +
                             "passwordRecovery.form.fields.enableEmailBasedRecovery.hint") as ReactNode }
                 </Hint>
+                <Heading as="h6">
+                    { t("extensions:manage.serverConfigurations.accountRecovery." +
+                    "passwordRecovery.form.fields.emailRecoveryOptions.header") as ReactNode }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/34e2e917aae565555d85c2f661b5e19633d56c20
created_at: 2025-03-20T11:17:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -354,6 +382,7 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
                     readOnly={ readOnly }
                     width={ 10 }
                     disabled={ !isConnectorEnabled }
+                    defaultValue = { isEmailRecoveryEnabled }
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/34e2e917aae565555d85c2f661b5e19633d56c20
created_at: 2025-03-20T11:18:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -153,7 +156,8 @@
             <div class="ui segment">
                     <%-- page content --%>
                     <h2>
-                    <%= i18n(recoveryResourceBundle, customText, "sms.otp.heading") %>
+                    <%= i18n(recoveryResourceBundle, customText,
+                            isEmailOtp ? "email.otp.heading" : "sms.otp.heading") %>
path: identity-apps-core/apps/recovery-portal/src/main/webapp/otp.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/5aa4668fe63c09d4e4937ef6028cef4f0c52506b
created_at: 2025-03-21T04:59:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +74,20 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: pavinduLakshan
body: ```suggestion
    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild[] = [
```
created_at: 2025-03-22T02:34:17Z
author_association: MEMBER
path: identity-apps-core/apps/recovery-portal/src/main/webapp/otp.jsp
user: login: pavinduLakshan
body: let's update the file name to sms-and-email-otp or something like that. otp.jsp could give a false impression that this page handles all otp authenticators.
created_at: 2025-03-22T02:38:20Z
author_association: MEMBER
diff_hunk: @@ -359,14 +359,23 @@
                 notificationChannel = recoveryResponse.getNotificationChannel();
                 request.setAttribute("callback", callback);
                 if (notificationChannel.equals("EMAIL")) {
+                    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                        request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant"));
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: pavinduLakshan
body: Let's encode the param 
created_at: 2025-03-22T02:40:48Z
author_association: MEMBER
diff_hunk: @@ -359,14 +359,23 @@
                 notificationChannel = recoveryResponse.getNotificationChannel();
                 request.setAttribute("callback", callback);
                 if (notificationChannel.equals("EMAIL")) {
+                    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                        request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant"));
+                    if (isEmailOtpBasedPasswordRecoveryEnabledByTenant) {
+                        request.setAttribute("channel", IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL);
+                        request.setAttribute("resendCode", recoveryResponse.getResendCode());
+                        request.setAttribute("flowConfirmationCode", recoveryResponse.getFlowConfirmationCode());
+                        request.getRequestDispatcher("otp.jsp").forward(request, response);
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: pavinduLakshan
body: Better if we can add a mapping in web.xml to give the jsp an end user friendly url path (eg: sms-otp.do)
created_at: 2025-03-22T02:41:58Z
author_association: MEMBER
diff_hunk: @@ -359,14 +359,23 @@
                 notificationChannel = recoveryResponse.getNotificationChannel();
                 request.setAttribute("callback", callback);
                 if (notificationChannel.equals("EMAIL")) {
+                    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                        request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant"));
+                    if (isEmailOtpBasedPasswordRecoveryEnabledByTenant) {
+                        request.setAttribute("channel", IdentityManagementEndpointConstants.PasswordRecoveryOptions.EMAIL);
+                        request.setAttribute("resendCode", recoveryResponse.getResendCode());
+                        request.setAttribute("flowConfirmationCode", recoveryResponse.getFlowConfirmationCode());
+                        request.getRequestDispatcher("otp.jsp").forward(request, response);
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: Malith-19
body: Currently we are showing this as `verify.do`. Do we still need mapping for specific otp page like (sms-otp.do and email-otp.do)
created_at: 2025-03-22T04:34:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -359,14 +359,23 @@
                 notificationChannel = recoveryResponse.getNotificationChannel();
                 request.setAttribute("callback", callback);
                 if (notificationChannel.equals("EMAIL")) {
+                    Boolean isEmailOtpBasedPasswordRecoveryEnabledByTenant = Boolean.parseBoolean(
+                        request.getParameter("isEmailOtpBasedPasswordRecoveryEnabledByTenant"));
path: identity-apps-core/apps/recovery-portal/src/main/webapp/recovery.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/e24dc88c471b8bb6301623e6f3dd54b62d004626
created_at: 2025-03-22T04:36:16Z
author_association: CONTRIBUTOR
path: identity-apps-core/apps/recovery-portal/src/main/webapp/otp.jsp
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/e24dc88c471b8bb6301623e6f3dd54b62d004626
created_at: 2025-03-22T04:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +74,20 @@ export const PasswordRecoveryConfigurationForm: FunctionComponent<PasswordRecove
     const [ isUpperCaseEnabled, setIsUpperCaseEnabled ] = useState<boolean>(false);
     const [ isLowerCaseEnabled, setIsLowerCaseEnabled ] = useState<boolean>(false);
     const [ isNumericEnabled, setIsNumericEnabled ] = useState<boolean>(false);
+    const [ emailRecoveryOption, setEmailRecoveryOption ] = useState<string>(EmailRecoveryOption.EMAIL_LINK);
+
+    const EMAIL_RECOVERY_RADIO_OPTIONS: RadioChild [] = [
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.tsx
user: login: Malith-19
body: Fixed by https://github.com/wso2/identity-apps/pull/7901/commits/e24dc88c471b8bb6301623e6f3dd54b62d004626
created_at: 2025-03-22T04:36:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,8 +38,12 @@
         div:nth-child(6),
         div:nth-child(7),
         div:nth-child(8),
-        div:nth-child(13),
-        div:nth-child(14) {
+        div:nth-child(9),
+        div:nth-child(10),
+        div:nth-child(11),
+        div:nth-child(12),
+        div:nth-child(17),
+        div:nth-child(18) {
         margin-left: 50px;
path: features/admin.server-configurations.v1/forms/password-recovery-form/password-recovery-form.scss
user: login: Malith-19
body: Tried using a class for this styling. But it caused to change the current layout as well.
created_at: 2025-03-23T17:27:32Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-18T03:06:52Z
body: ## [Codecov](https://app.codecov.io/gh/wso2/identity-apps/pull/7901?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 41.85%. Comparing base [(`4360cdb`)](https://app.codecov.io/gh/wso2/identity-apps/commit/4360cdb907da60f3541673fb33140d8d45d740c7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2) to head [(`e24dc88`)](https://app.codecov.io/gh/wso2/identity-apps/commit/e24dc88c471b8bb6301623e6f3dd54b62d004626?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2).
> Report is 1106 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7901   +/-   ##
=======================================
  Coverage   41.85%   41.85%           
=======================================
  Files          42       42           
  Lines         939      939           
  Branches      238      238           
=======================================
  Hits          393      393           
- Misses        502      546   +44     
+ Partials       44        0   -44     
```

| [Flag](https://app.codecov.io/gh/wso2/identity-apps/pull/7901/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2) | Coverage Δ | |
|---|---|---|
| [@wso2is/core](https://app.codecov.io/gh/wso2/identity-apps/pull/7901/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2) | `41.85% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2#carryforward-flags-in-the-pull-request-comment) to find out more.

[see 12 files with indirect coverage changes](https://app.codecov.io/gh/wso2/identity-apps/pull/7901/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wso2)

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: wso2-jenkins-bot
created_at: 2025-03-22T04:36:20Z
body: <h3>🦋 Changeset detected</h3><p><b>The changes in this PR will be included in the next version bump.</b></p><p>Not sure what this means? <a href="https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md">Click here to learn what changesets are</a>.</p>
author_association: CONTRIBUTOR