Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { TestableComponentInterface,
import { Field, FormValue, Forms } from "@wso2is/forms/legacy";
import { GenericIcon } from "@wso2is/react-components";
import { AxiosError } from "axios";
import React, { ReactElement, useEffect, useState } from "react";
import React, { MutableRefObject, ReactElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { Dispatch } from "redux";
Expand Down Expand Up @@ -70,7 +70,8 @@ export const SecurityQuestionsComponent: React.FunctionComponent<SecurityQuestio
const [ challengeQuestions, setChallengeQuestions ] = useState<ChallengesQuestionsInterface[]>();
const [ challenges, setChallenges ] = useState(createEmptyChallenge());
const [ isEdit, setIsEdit ] = useState<number | string>(-1);
const [ isInit, setIsInit ] = useState(false);
// Guards a one-time fetch on mount; only read/written in handlers, never rendered, so a ref avoids a re-render.
const isInitRef: MutableRefObject<boolean> = useRef<boolean>(false);

const activeForm: string = useSelector((state: AppState) => state.global.activeForm);

Expand All @@ -82,7 +83,7 @@ export const SecurityQuestionsComponent: React.FunctionComponent<SecurityQuestio
* @param response - security questions API response
*/
const setSecurityDetails = (response: any) => {
setIsInit(true);
isInitRef.current = true;
setChallenges({
answers: [ ...response[1] ],
isEdit: false,
Expand Down Expand Up @@ -292,7 +293,7 @@ export const SecurityQuestionsComponent: React.FunctionComponent<SecurityQuestio
};

useEffect(() => {
if (!isInit) {
if (!isInitRef.current) {
getSecurityQs().then((response: any) => {
setSecurityDetails(response);
});
Expand Down
13 changes: 7 additions & 6 deletions apps/myaccount/src/components/consents/consents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { TestableComponentInterface } from "@wso2is/core/models";
import cloneDeep from "lodash-es/cloneDeep";
import flatten from "lodash-es/flatten";
import React, { FunctionComponent, useEffect, useState } from "react";
import React, { FunctionComponent, MutableRefObject, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import { Message, Modal } from "semantic-ui-react";
Expand Down Expand Up @@ -70,7 +70,8 @@ export const Consents: FunctionComponent<ConsentComponentProps> = (props: Consen
const { onAlertFired, ["data-testid"]: testId } = props;

const [ consentedApps, setConsentedApps ] = useState<ConsentInterface[]>([]);
const [ purposeDetailModels, setPurposeDetailModels ] = useState<PurposeModel[]>([]);
// Cache of purpose models, only read/written inside handlers, so a ref avoids unnecessary re-renders.
const purposeDetailModelsRef: MutableRefObject<PurposeModel[]> = useRef<PurposeModel[]>([]);
const [ revokingConsent, setRevokingConsent ] = useState<ConsentInterface>();
const [ isConsentRevokeModalVisible, setConsentRevokeModalVisibility ] = useState(false);
const [ consentListActiveIndexes, setConsentListActiveIndexes ] = useState([]);
Expand Down Expand Up @@ -152,7 +153,7 @@ export const Consents: FunctionComponent<ConsentComponentProps> = (props: Consen
const response: PurposeModel[] = await fetchPurposesByIDs(purposesIds);

// Set response value to the hook
setPurposeDetailModels(response);
purposeDetailModelsRef.current = response;
};

/**
Expand Down Expand Up @@ -211,7 +212,7 @@ export const Consents: FunctionComponent<ConsentComponentProps> = (props: Consen
const attachResidentIDPReceiptMissingPurposes = async (receipt: ConsentReceiptInterface): Promise<void> => {

// Filter out the non-default purposes from the cached {@link purposeModels}
const allPurposeModelsExceptDefault: PurposeModel[] = purposeDetailModels.filter(
const allPurposeModelsExceptDefault: PurposeModel[] = purposeDetailModelsRef.current.filter(
({ purpose }: {
purpose: string
}) => purpose !== ConsentConstants.DEFAULT_CONSENT
Expand Down Expand Up @@ -283,7 +284,7 @@ export const Consents: FunctionComponent<ConsentComponentProps> = (props: Consen
});
});

const allPurposesExceptDefault: PurposeModel[] = purposeDetailModels.filter(
const allPurposesExceptDefault: PurposeModel[] = purposeDetailModelsRef.current.filter(
({ purpose }: {
purpose: string
}) => purpose !== ConsentConstants.DEFAULT_CONSENT
Expand Down Expand Up @@ -412,7 +413,7 @@ export const Consents: FunctionComponent<ConsentComponentProps> = (props: Consen
await attachResidentIDPReceiptMissingPurposes(receipt);
await attachResidentIDPReceiptPurposes(receipt);
} else {
const defaultPurpose: PurposeModel = purposeDetailModels.find(
const defaultPurpose: PurposeModel = purposeDetailModelsRef.current.find(
({ purpose }: { purpose: string }) => purpose === ConsentConstants.DEFAULT_CONSENT
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { TestableComponentInterface,
} from "@wso2is/core/models";
import { AppAvatar, Popup } from "@wso2is/react-components";
import { AxiosError } from "axios";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import React, { FunctionComponent, MutableRefObject, ReactElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Grid, Icon, List, Modal } from "semantic-ui-react";
import { deleteFederatedAssociation, getFederatedAssociations } from "../../api/federated-associations";
Expand Down Expand Up @@ -69,7 +69,8 @@ export const FederatedAssociations: FunctionComponent<FederatedAssociationsProps
} = props;

const [ confirmDelete, setConfirmDelete ] = useState(false);
const [ id, setId ] = useState(null);
// Holds the association id targeted by the delete-confirmation modal; only read/written in handlers.
const idRef: MutableRefObject<string> = useRef<string>(null);
const { t } = useTranslation();
const [ federatedAssociations, setFederatedAssociations ] = useState<FederatedAssociation[]>([]);
const [ showExternalLogins, setShowExternalLogins ] = useState<boolean>(true);
Expand Down Expand Up @@ -207,7 +208,7 @@ export const FederatedAssociations: FunctionComponent<FederatedAssociationsProps
<Button
className="link-button"
onClick={ () => {
setId(null);
idRef.current = null;
setConfirmDelete(false);
} }
>
Expand All @@ -217,9 +218,9 @@ export const FederatedAssociations: FunctionComponent<FederatedAssociationsProps
primary
onClick={ () => {
removeFederatedAssociation(
id
idRef.current
);
setId(null);
idRef.current = null;
setConfirmDelete(false);
} }
>
Expand Down Expand Up @@ -292,7 +293,7 @@ export const FederatedAssociations: FunctionComponent<FederatedAssociationsProps
color="grey"
name="trash alternate"
onClick={ () => {
setId(federatedAssociation.id);
idRef.current = federatedAssociation.id;
setConfirmDelete(true);
} }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { TestableComponentInterface } from "@wso2is/core/models";
import { Field, FormValue, Forms } from "@wso2is/forms/legacy";
import React, { FunctionComponent, ReactElement, useState } from "react";
import React, { FunctionComponent, MutableRefObject, ReactElement, useRef } from "react";
import { useTranslation } from "react-i18next";
import { Form, Grid, Input, InputOnChangeData } from "semantic-ui-react";
import { UIConstants } from "../../constants";
Expand Down Expand Up @@ -59,15 +59,16 @@ export const LinkedAccountsEdit: FunctionComponent<LinkedAccountsEditProps> = (

const { t } = useTranslation();

const [ userName, setUserName ] = useState<string>(undefined);
// Captures the latest typed username; read only on submit, never rendered, so a ref avoids per-keystroke re-renders.
const userNameRef: MutableRefObject<string> = useRef<string>(undefined);
Comment on lines +62 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -HI 'tsconfig*.json' -t f -x rg -n '"strictNullChecks"|"strict"' {}
rg -n 'useRef<string>\((null|undefined)\)' apps/myaccount/src/components

Repository: wso2/identity-apps

Length of output: 2909


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
wc -l apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx apps/myaccount/src/components/federated-associations/federated-associations.tsx

echo "== edit relevant =="
cat -n apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx | sed -n '1,130p'

echo "== list relevant =="
cat -n apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx | sed -n '1,130p'

echo "== federated relevant =="
cat -n apps/myaccount/src/components/federated-associations/federated-associations.tsx | sed -n '1,260p'

echo "== references to values =="
rg -n 'userNameRef|userIDRef|idRef|LinkedAccount|onLinkedAccountRemove|removeFederatedAssociation' apps/myaccount/src/components/linked-accounts apps/myaccount/src/components/federated-associations

Repository: wso2/identity-apps

Length of output: 33976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tsconfig files relevant to app =="
for f in package.json apps/myaccount/package.json apps/myaccount/tsconfig.json apps/myaccount/tsconfig.app.json apps/myaccount/tsconfig.dev.json tsconfig.base.json tsconfig.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    sed -n '/"compilerOptions"/,/^[[:space:]]*}/p' "$f" 2>/dev/null | head -80
  fi
done

echo "== edit remaining lines =="
cat -n apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx | sed -n '130,175p'

echo "== federated remaining relevant lines =="
cat -n apps/myaccount/src/components/federated-associations/federated-associations.tsx | sed -n '260,295p'

Repository: wso2/identity-apps

Length of output: 7201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== vite config files =="
fd -HI 'vite.config.*' . -t f -x sh -c 'echo "--- $1 ---"; sed -n "1,220p" "$1"' sh {}

echo "== tsconfig inheritance =="
for f in apps/myaccount/tsconfig.json tsconfig.base.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat "$f"
  fi
done

echo "== exact compiler options in apps/myaccount/tsconfig.json =="
python3 - <<'PY'
import json
from pathlib import Path
p=Path("apps/myaccount/tsconfig.json")
data=json.loads(p.read_text())
print(json.dumps(data, indent=2))
PY

echo "== compile-time availability without installing =="
node - <<'JS'
try {
  const ts = require("typescript");
  console.log("typescript available", ts.version);
} catch (e) {
  console.log("typescript not available", e.message);
}
JS

Repository: wso2/identity-apps

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== root/react tsconfig null-related options =="
for f in tsconfig.base.react.json tsconfig.base.json apps/myaccount/tsconfig.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '"strict"|"strictNullChecks"|"strictPropertyInitialization"|"exactOptionalPropertyTypes"|"paths"|"extends"' "$f" || true
  fi
done

echo "== all tsconfig null-related options =="
rg -n --glob 'tsconfig*.json' '"strict"|"strictNullChecks"|"strictPropertyInitialization"|"exactOptionalPropertyTypes"' .

Repository: wso2/identity-apps

Length of output: 454


Keep handler-only ref contents nullable.

userIDRef and idRef are typed as non-nullable strings but are initialized/cleared with null; narrow/refine before passing them into handleLinkedAccountRemove() and removeFederatedAssociation(). For userNameRef, initialize with "" or validate the missing value before building getFormValues() so submit cannot receive a non-string username.

📍 Affects 3 files
  • apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx#L62-L63 (this comment)
  • apps/myaccount/src/components/linked-accounts/linked-accounts-list.tsx#L57-L58
  • apps/myaccount/src/components/federated-associations/federated-associations.tsx#L72-L73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/myaccount/src/components/linked-accounts/linked-accounts-edit.tsx`
around lines 62 - 63, Keep handler-only refs nullable in
linked-accounts-edit.tsx at lines 62-63, and initialize userNameRef with an
empty string or validate it before constructing getFormValues(). In
linked-accounts-list.tsx lines 57-58, narrow userIDRef before passing it to
handleLinkedAccountRemove(); in federated-associations.tsx lines 72-73, narrow
idRef before passing it to removeFederatedAssociation().


/**
*
* @param event - Username change event.
* @param data - Input field data.
*/
const handleUsernameChange = (event: React.ChangeEvent<HTMLInputElement>, data: InputOnChangeData): void => {
setUserName(data.value);
userNameRef.current = data.value;
};

/**
Expand All @@ -77,7 +78,7 @@ export const LinkedAccountsEdit: FunctionComponent<LinkedAccountsEditProps> = (
const getFormValues = (values: Map<string, FormValue>): void => {
const formValues: { password: string; username: string } = {
password: values.get("password").toString(),
username: userName
username: userNameRef.current
};

onFormSubmit(formValues, UIConstants.ADD_LOCAL_LINKED_ACCOUNT_FORM_IDENTIFIER);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import { TestableComponentInterface } from "@wso2is/core/models";
import { Popup } from "@wso2is/react-components";
import React, { FunctionComponent, useState } from "react";
import React, { FunctionComponent, MutableRefObject, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Grid, Icon, List, Modal } from "semantic-ui-react";
import { getGravatarImage } from "../../api";
Expand Down Expand Up @@ -54,7 +54,8 @@ export const LinkedAccountsList: FunctionComponent<LinkedAccountsListProps> = (
["data-testid"]: testId
} = props;
const [ confirmDelete, setConfirmDelete ] = useState(false);
const [ userID, setUserID ] = useState(null);
// Holds the account id targeted by the delete-confirmation modal; only read/written in handlers.
const userIDRef: MutableRefObject<string> = useRef<string>(null);

const { t } = useTranslation();

Expand All @@ -77,7 +78,7 @@ export const LinkedAccountsList: FunctionComponent<LinkedAccountsListProps> = (
<Button
onClick={ () => {
setConfirmDelete(false);
setUserID(null);
userIDRef.current = null;
} }
className="link-button"
>
Expand All @@ -86,9 +87,9 @@ export const LinkedAccountsList: FunctionComponent<LinkedAccountsListProps> = (
<Button
primary
onClick={ () => {
onLinkedAccountRemove(userID);
onLinkedAccountRemove(userIDRef.current);
setConfirmDelete(false);
setUserID(null);
userIDRef.current = null;
} }
>
{ t("common:remove") }
Expand Down Expand Up @@ -151,7 +152,7 @@ export const LinkedAccountsList: FunctionComponent<LinkedAccountsListProps> = (
color="red"
name="trash alternate outline"
onClick={ () => {
setUserID(account.userId);
userIDRef.current = account.userId;
setConfirmDelete(true);
} }
/>
Expand Down
17 changes: 13 additions & 4 deletions apps/myaccount/src/components/user-sessions/user-sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ import { EmphasizedSegment } from "@wso2is/react-components";
import { AxiosError } from "axios";
import reverse from "lodash-es/reverse";
import sortBy from "lodash-es/sortBy";
import React, { FunctionComponent, MouseEvent, ReactElement, useEffect, useState } from "react";
import React, {
FunctionComponent,
MouseEvent,
MutableRefObject,
ReactElement,
useEffect,
useRef,
useState
} from "react";
import { useTranslation } from "react-i18next";
import { Button, ButtonProps, Container, Modal, Placeholder } from "semantic-ui-react";
import { UserSessionsList } from "./user-sessions-list";
Expand Down Expand Up @@ -63,7 +71,8 @@ export const UserSessionsComponent: FunctionComponent<UserSessionsComponentProps
const { t } = useTranslation();

const [ userSessions, setUserSessions ] = useState<UserSessions>(emptyUserSessions);
const [ editingUserSession, setEditingUserSession ] = useState<UserSession>(emptyUserSession);
// Holds the session targeted by the terminate-confirmation modal; only read/written in handlers.
const editingUserSessionRef: MutableRefObject<UserSession> = useRef<UserSession>(emptyUserSession());
const [ isRevokeAllUserSessionsModalVisible, setRevokeAllUserSessionsModalVisibility ] = useState(false);
const [ isRevokeUserSessionModalVisible, setRevokeUserSessionModalVisibility ] = useState(false);
const [ sessionsListActiveIndexes, setSessionsListActiveIndexes ] = useState([]);
Expand Down Expand Up @@ -156,7 +165,7 @@ export const UserSessionsComponent: FunctionComponent<UserSessionsComponentProps
* Terminate a single user session.
*/
const handleTerminateUserSession = (): void => {
terminateUserSession(editingUserSession.id)
terminateUserSession(editingUserSessionRef.current.id)
.then(() => {
onAlertFired({
description: t(
Expand Down Expand Up @@ -263,7 +272,7 @@ export const UserSessionsComponent: FunctionComponent<UserSessionsComponentProps
* @param session - Session which needs to be edited.
*/
const handleTerminateUserSessionClick = (session: UserSession): void => {
setEditingUserSession(session);
editingUserSessionRef.current = session;
setRevokeUserSessionModalVisibility(true);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import {
PageLayout } from "@wso2is/react-components";
import { AxiosError } from "axios";
import isEmpty from "lodash-es/isEmpty";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import React, { FunctionComponent, MutableRefObject, ReactElement, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { Dispatch } from "redux";
Expand Down Expand Up @@ -110,7 +110,10 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
];
const [ isAlphanumericUsername, setIsAlphanumericUsername ] = useState<boolean>(false);
const [ showConfirmationModal, setShowConfirmationModal ] = useState<boolean>(false);
const [ pendingFormValues, setPendingFormValues ] = useState<AlternativeLoginIdentifierFormInterface>(null);
// Holds form values awaiting user consent in the confirmation modal. Only written and read inside
// handlers (never rendered), so a ref avoids an unnecessary re-render on each update.
const pendingFormValuesRef: MutableRefObject<AlternativeLoginIdentifierFormInterface> =
useRef<AlternativeLoginIdentifierFormInterface>(null);

const {
data: validationData
Expand Down Expand Up @@ -517,7 +520,7 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde

// Show confirmation modal if uniqueness scope update is required and no consent received yet.
if (!hasUserConsent && requiresUniquenessScopeUpdate) {
setPendingFormValues(formValues);
pendingFormValuesRef.current = formValues;
setShowConfirmationModal(true);

return;
Expand All @@ -537,11 +540,11 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
* Handles the form submission after user consents to uniqueness scope update.
*/
const handleConsentedSubmit = (): void => {
if (!pendingFormValues) {
if (!pendingFormValuesRef.current) {
return;
}

processFormSubmission(pendingFormValues, true);
processFormSubmission(pendingFormValuesRef.current, true);
setShowConfirmationModal(false);
};

Expand Down
Loading