From 32e6c0910a4221dc81069c74759bb6d93fbd0ab0 Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 04:54:19 +0000 Subject: [PATCH 01/11] feat: Expand GBR/RBR with an "action badge" --- src/CONST/index.ts | 6 ++ src/components/Badge.tsx | 4 +- .../LHNOptionsList/OptionRowLHN.tsx | 65 ++++++++++++------- .../LHNOptionsList/OptionRowLHNData.tsx | 1 + src/languages/en.ts | 6 ++ src/libs/ReportUtils.ts | 11 ++-- src/libs/SidebarUtils.ts | 1 + src/libs/actions/IOU/index.ts | 40 +++++++++--- .../OnyxDerived/configs/reportAttributes.ts | 12 +++- src/styles/utils/index.ts | 2 +- src/types/onyx/DerivedValues.ts | 4 ++ tests/actions/IOUTest.ts | 6 +- 12 files changed, 115 insertions(+), 43 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ddb994ba8f988..404879819ebe9 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1267,6 +1267,12 @@ const CONST = { ADD_UNREPORTED_EXPENSE: 'addUnreportedExpense', TRACK_DISTANCE_EXPENSE: 'trackDistanceExpense', }, + ACTION_BADGE: { + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', + FIX: 'fix', + }, ACTIONS: { LIMIT: 50, // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index bfdc63cf71193..444c03108e962 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -85,11 +85,11 @@ function Badge({ styles.defaultBadge, isCondensed && styles.condensedBadge, styles.alignSelfCenter, - styles.ml2, + styles.ml1, StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC, isStrong), badgeStyles, ], - [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml2, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong], + [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml1, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong], ); return ( diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 5102cb35bf3e5..2f75c3c5f75ba 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,6 +1,7 @@ import React, {useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; +import Badge from '@components/Badge'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -12,6 +13,7 @@ import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -55,6 +57,7 @@ function OptionRowLHN({ testID, conciergeReportID, }: OptionRowLHNProps) { + const {isProduction} = useEnvironment(); const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -158,6 +161,7 @@ function OptionRowLHN({ } const brickRoadIndicator = optionItem.brickRoadIndicator; + const actionBadgeText = !isProduction && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, styles.flexShrink0, style]; @@ -376,25 +380,42 @@ function OptionRowLHN({ ) : null} {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( - + {actionBadgeText ? ( + + ) : ( + + )} )} - {brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO && ( - - - - )} + ) : ( + + + + ))} {hasDraftComment && !!optionItem.isAllowedToComment && ( )} - {!brickRoadIndicator && !!optionItem.isPinned && ( - - - + {!isProduction && !brickRoadIndicator && !!optionItem.isPinned && ( + )} diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 2467c721d31f0..afaaee65c4098 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -113,6 +113,7 @@ function OptionRowLHNData({ }, [ fullReport, reportAttributes?.brickRoadStatus, + reportAttributes?.actionBadge, reportAttributes?.reportName, areReportErrorsEqual, oneTransactionThreadReport, diff --git a/src/languages/en.ts b/src/languages/en.ts index fc93250f63a26..98aad7339deb6 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -191,6 +191,12 @@ const translations = { home: 'Home', inbox: 'Inbox', yourReviewIsRequired: 'Your review is required', + actionBadge: { + submit: 'Submit', + approve: 'Approve', + pay: 'Pay', + fix: 'Fix', + }, // @context Used in confirmation or result messages indicating that an action completed successfully, not the abstract noun “success.” success: 'Success', group: 'Group', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f8d544f17bef7..c04312ccd3696 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -92,7 +92,7 @@ import { canIOUBePaid, canSubmitReport, createDraftTransaction, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, setMoneyRequestParticipants, setMoneyRequestParticipantsFromReport, setMoneyRequestReportID, @@ -4321,7 +4321,7 @@ function getReasonAndReportActionThatRequiresAttention( // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated const invoiceReceiverPolicy = invoiceReceiverPolicyID ? getPolicy(invoiceReceiverPolicyID) : undefined; - const iouReportActionToApproveOrPay = getIOUReportActionToApproveOrPay(optionOrReport, undefined, optionReportMetadata, invoiceReceiverPolicy); + const {reportAction: iouReportActionToApproveOrPay, actionBadge} = getIOUReportActionWithBadge(optionOrReport, undefined, optionReportMetadata, invoiceReceiverPolicy); const iouReportID = getIOUReportIDFromReportActionPreview(iouReportActionToApproveOrPay); const transactions = getReportTransactions(iouReportID); const hasOnlyPendingTransactions = transactions.length > 0 && transactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); @@ -4337,6 +4337,7 @@ function getReasonAndReportActionThatRequiresAttention( return { reason: CONST.REQUIRES_ATTENTION_REASONS.HAS_CHILD_REPORT_AWAITING_ACTION, reportAction: iouReportActionToApproveOrPay, + actionBadge, }; } @@ -12699,6 +12700,7 @@ function generateReportAttributes({ reportActions?: OnyxCollection; transactionViolations: OnyxCollection; isReportArchived: boolean; + actionBadge?: ValueOf; }) { const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`]; const parentReportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`]; @@ -12708,7 +12710,7 @@ function generateReportAttributes({ const hasErrors = Object.entries(reportErrors ?? {}).length > 0; const oneTransactionThreadReportID = getOneTransactionThreadReportID(report, chatReport, reportActionsList); const parentReportAction = report?.parentReportActionID ? parentReportActionsList?.[report.parentReportActionID] : undefined; - const requiresAttention = requiresAttentionFromCurrentUser(report, parentReportAction, isReportArchived); + const {reason, actionBadge} = getReasonAndReportActionThatRequiresAttention(report, parentReportAction, isReportArchived) ?? {}; return { hasViolationsToDisplayInLHN, @@ -12717,7 +12719,8 @@ function generateReportAttributes({ hasErrors, oneTransactionThreadReportID, parentReportAction, - requiresAttention, + requiresAttention: !!reason, + actionBadge, }; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5351d9094ff96..b8f99972132e6 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -791,6 +791,7 @@ function getOptionData({ result.shouldShowSubscript = rawShouldShowSubscript && !threadSuppression && !taskSuppression; result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; result.brickRoadIndicator = reportAttributes?.brickRoadStatus; + result.actionBadge = reportAttributes?.actionBadge; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 527ac81709797..29f49e7e7d576 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -131,6 +131,7 @@ import { buildOptimisticSubmittedReportAction, buildOptimisticUnapprovedReportAction, canBeAutoReimbursed, + canSubmitAndIsAwaitingForCurrentUser, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, findSelfDMReportID, generateReportID, @@ -9785,16 +9786,17 @@ function canSubmitReport( ); } -function getIOUReportActionToApproveOrPay( +function getIOUReportActionWithBadge( chatReport: OnyxEntry, updatedIouReport: OnyxEntry, reportMetadata: OnyxEntry, invoiceReceiverPolicy: OnyxEntry, -): OnyxEntry { +): {reportAction: OnyxEntry; actionBadge?: ValueOf} { const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; - return Object.values(chatReportActions).find((action) => { - if (!action) { + let actionBadge: ValueOf | undefined; + const reportAction = Object.values(chatReportActions).find((action) => { + if (!action || action.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW || isDeletedAction(action)) { return false; } const iouReport = updatedIouReport?.reportID === action.childReportID ? updatedIouReport : getReportOrDraftReport(action.childReportID); @@ -9802,10 +9804,32 @@ function getIOUReportActionToApproveOrPay( // eslint-disable-next-line @typescript-eslint/no-deprecated const policy = getPolicy(iouReport?.policyID); // Only show to the actual payer, exclude admins with bank account access - const shouldShowSettlementButton = - canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy) || canApproveIOU(iouReport, policy, reportMetadata); - return action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && shouldShowSettlementButton && !isDeletedAction(action); + if (canIOUBePaid(iouReport, chatReport, policy, undefined, undefined, undefined, undefined, invoiceReceiverPolicy)) { + actionBadge = CONST.REPORT.ACTION_BADGE.PAY; + return true; + } + if (canApproveIOU(iouReport, policy, reportMetadata)) { + actionBadge = CONST.REPORT.ACTION_BADGE.APPROVE; + return true; + } + const isWaitingSubmitFromCurrentUser = canSubmitAndIsAwaitingForCurrentUser( + iouReport, + chatReport, + policy, + getReportTransactions(iouReport?.reportID), + allTransactionViolations, + currentUserEmail, + userAccountID, + getAllReportActions(iouReport?.reportID), + ); + if (isWaitingSubmitFromCurrentUser) { + actionBadge = CONST.REPORT.ACTION_BADGE.SUBMIT; + return true; + } + return false; }); + + return {reportAction, actionBadge}; } /** @@ -13167,7 +13191,7 @@ export { updateMoneyRequestTaxRate, updateLastLocationPermissionPrompt, shouldOptimisticallyUpdateSearch, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, getNavigationUrlOnMoneyRequestDelete, getNavigationUrlAfterTrackExpenseDelete, canSubmitReport, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 899d8c5187817..70aced89f70e2 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -198,7 +198,13 @@ export default createOnyxDerivedValueConfig({ const reportNameValuePair = reportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; const reportActionsList = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`]; const isReportArchived = isArchivedReport(reportNameValuePair); - const {hasAnyViolations, requiresAttention, reportErrors, oneTransactionThreadReportID} = generateReportAttributes({ + const { + hasAnyViolations, + requiresAttention, + reportErrors, + oneTransactionThreadReportID, + actionBadge: actionGreenBadge, + } = generateReportAttributes({ report, chatReport, reportActions, @@ -210,6 +216,7 @@ export default createOnyxDerivedValueConfig({ const hasFieldViolations = hasVisibleReportFieldViolations(report, policy, reportViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]); let brickRoadStatus; + let actionBadge; // if report has errors or violations, show red dot if ( SidebarUtils.shouldShowRedBrickRoad( @@ -224,10 +231,12 @@ export default createOnyxDerivedValueConfig({ ) ) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } // if report does not have error, check if it should show green dot if (brickRoadStatus !== CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && requiresAttention) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + actionBadge = actionGreenBadge; } acc[report.reportID] = { @@ -248,6 +257,7 @@ export default createOnyxDerivedValueConfig({ isEmpty: generateIsEmptyReport(report, isReportArchived), brickRoadStatus, requiresAttention, + actionBadge, reportErrors, oneTransactionThreadReportID, }; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 37ef7e9550ec5..885ad9cc579e3 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1502,7 +1502,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getIconColorStyle: (isSuccess: boolean, isError: boolean, isStrong = false): string => { if (isStrong) { - return theme.white; + return theme.icon; } if (isSuccess) { return theme.badgeSuccessText; diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 114c11f0b4e63..379cf0ce8a336 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -29,6 +29,10 @@ type ReportAttributes = { * Whether the report requires attention from current user. */ requiresAttention: boolean; + /** + * The action badge to display instead of the GBR/RBR dot (e.g. 'submit', 'approve', 'pay'). + */ + actionBadge?: ValueOf; /** * The errors of the report. */ diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 21805f5853915..1b585bd71be0e 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -23,7 +23,7 @@ import { createDistanceRequest, deleteMoneyRequest, getDeleteTrackExpenseInformation, - getIOUReportActionToApproveOrPay, + getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, getReportPreviewAction, getTrackExpenseInformation, @@ -13315,7 +13315,7 @@ describe('actions/IOU', () => { }); }); - describe('getIOUReportActionToApproveOrPay', () => { + describe('getIOUReportActionWithBadge', () => { it('should exclude deleted actions', async () => { const reportID = '1'; const policyID = '2'; @@ -13389,7 +13389,7 @@ describe('actions/IOU', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); - expect(getIOUReportActionToApproveOrPay(fakeReport, undefined, {}, undefined)).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + expect(getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined)).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); }); }); From e7b3cd673846ba9aabe6f09daa89cde2786e8301 Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 05:02:48 +0000 Subject: [PATCH 02/11] fix lint --- .../LHNOptionsList/OptionRowLHN.tsx | 29 ++++++++++++++----- src/libs/ReportUtils.ts | 1 + 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 2f75c3c5f75ba..5c21fa2932887 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -428,14 +428,27 @@ function OptionRowLHN({ /> )} - {!isProduction && !brickRoadIndicator && !!optionItem.isPinned && ( - - )} + {!brickRoadIndicator && + !!optionItem.isPinned && + (isProduction ? ( + + + + ) : ( + + ))} ); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c04312ccd3696..1999f27a5239f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4243,6 +4243,7 @@ function isUnreadWithMention(reportOrOption: OnyxEntry | OptionData): bo type ReasonAndReportActionThatRequiresAttention = { reason: ValueOf; reportAction?: OnyxEntry; + actionBadge?: ValueOf; }; /** From 9d4f8c3eeeb00d0e760ff6f945a32a7918b65f3f Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 05:32:42 +0000 Subject: [PATCH 03/11] address feedback --- src/libs/ReportUtils.ts | 1 + src/libs/actions/OnyxDerived/configs/reportAttributes.ts | 1 + tests/actions/IOUTest.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1999f27a5239f..fdfe1e2be3ef8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -830,6 +830,7 @@ type OptionData = { alternateText?: string; allReportErrors?: Errors; brickRoadIndicator?: ValueOf | '' | null; + actionBadge?: ValueOf; tooltipText?: string | null; alternateTextMaxLines?: number; boldStyle?: boolean; diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 70aced89f70e2..2e2426f5b5888 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -287,6 +287,7 @@ export default createOnyxDerivedValueConfig({ } reportAttributes[chatReportID].brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + reportAttributes[chatReportID].actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } // mark the report attributes as fully computed after first iteration to avoid unnecessary recomputation on all objects diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 1b585bd71be0e..c50bc8c1ee760 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -13389,7 +13389,7 @@ describe('actions/IOU', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); - expect(getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined)).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + expect(getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined).reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); }); }); From ed089301839006400fdfb4394102b56784316ddf Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 06:10:25 +0000 Subject: [PATCH 04/11] disable gate for codex review --- src/components/LHNOptionsList/OptionRowLHN.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 5c21fa2932887..8cbdf62db430a 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -58,6 +58,7 @@ function OptionRowLHN({ conciergeReportID, }: OptionRowLHNProps) { const {isProduction} = useEnvironment(); + const canShowBadge = true; const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -161,7 +162,7 @@ function OptionRowLHN({ } const brickRoadIndicator = optionItem.brickRoadIndicator; - const actionBadgeText = !isProduction && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; + const actionBadgeText = canShowBadge && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, styles.flexShrink0, style]; @@ -430,7 +431,7 @@ function OptionRowLHN({ )} {!brickRoadIndicator && !!optionItem.isPinned && - (isProduction ? ( + (!canShowBadge ? ( Date: Thu, 12 Mar 2026 07:39:12 +0000 Subject: [PATCH 05/11] enable production gate back --- src/components/LHNOptionsList/OptionRowLHN.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 8cbdf62db430a..5c21fa2932887 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -58,7 +58,6 @@ function OptionRowLHN({ conciergeReportID, }: OptionRowLHNProps) { const {isProduction} = useEnvironment(); - const canShowBadge = true; const theme = useTheme(); const styles = useThemeStyles(); const popoverAnchor = useRef(null); @@ -162,7 +161,7 @@ function OptionRowLHN({ } const brickRoadIndicator = optionItem.brickRoadIndicator; - const actionBadgeText = canShowBadge && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; + const actionBadgeText = !isProduction && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, styles.flexShrink0, style]; @@ -431,7 +430,7 @@ function OptionRowLHN({ )} {!brickRoadIndicator && !!optionItem.isPinned && - (!canShowBadge ? ( + (isProduction ? ( Date: Thu, 12 Mar 2026 08:39:06 +0000 Subject: [PATCH 06/11] recompute report attributes when policies are loaded first time --- .../actions/OnyxDerived/configs/reportAttributes.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 2e2426f5b5888..1cf8d7b402f9d 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {computeReportName} from '@libs/ReportNameUtils'; import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isValidReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -6,11 +6,12 @@ import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDer import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, ReportAttributesDerivedValue} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, ReportAttributesDerivedValue} from '@src/types/onyx'; let isFullyComputed = false; let previousDisplayNames: Record = {}; let previousPersonalDetails: OnyxEntry | undefined; +let previousPolicies: OnyxCollection; const prepareReportKeys = (keys: string[]) => { return [ @@ -98,6 +99,14 @@ export default createOnyxDerivedValueConfig({ isFullyComputed = false; } + // if policies are loaded first time, we need to recompute all report attributes to get correct action badge in LHN, such as Approve because it depends on policy's type (see canApproveIOU function) + if (hasKeyTriggeredCompute(ONYXKEYS.COLLECTION.POLICY, sourceValues)) { + if (isFullyComputed && Object.keys(previousPolicies ?? {}).length === 0 && Object.keys(policies ?? {}).length > 0) { + isFullyComputed = false; + } + previousPolicies = policies; + } + // if we already computed the report attributes and there is no new reports data, return the current value if ((isFullyComputed && !sourceValues) || !reports) { return currentValue ?? {reports: {}, locale: null}; From f7004010b484ed3064637def37723d6b338af9dc Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 10:13:47 +0000 Subject: [PATCH 07/11] add unit test --- tests/actions/IOUTest.ts | 248 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 247 insertions(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index c50bc8c1ee760..061d1acb2b5c0 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -13389,7 +13389,253 @@ describe('actions/IOU', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fakeReport.reportID}`, MOCK_REPORT_ACTIONS); - expect(getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined).reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + const result = getIOUReportActionWithBadge(fakeReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(MOCK_REPORT_ACTIONS[reportID]); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return APPROVE actionBadge for submitted expense report when user is manager', async () => { + const chatReportID = '100'; + const iouReportID = '101'; + const policyID = '102'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.APPROVE); + }); + + it('should return PAY actionBadge for approved expense report when user is payer', async () => { + const chatReportID = '200'; + const iouReportID = '201'; + const policyID = '202'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: -10000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should return SUBMIT actionBadge for open report waiting for submission', async () => { + const chatReportID = '300'; + const iouReportID = '301'; + const policyID = '302'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.USER, + harvesting: {enabled: false}, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + isOwnPolicyExpenseChat: true, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + ownerAccountID: RORY_ACCOUNT_ID, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + merchant: 'TestMerchant', + modifiedMerchant: 'TestMerchant', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.SUBMIT); + }); + + it('should return undefined actionBadge when report is settled', async () => { + const chatReportID = '400'; + const iouReportID = '401'; + const policyID = '402'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + // Settled (reimbursed) report — can't pay, can't approve, can't submit + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + managerID: RORY_ACCOUNT_ID, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 100, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + + const reportPreviewAction = { + reportActionID: iouReportID, + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + message: [{type: 'TEXT', text: 'Report preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); }); }); From d8c356995bba89343fd99f5b579b7cecec18a8e2 Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 14:54:44 +0000 Subject: [PATCH 08/11] update badge style --- src/components/Badge.tsx | 2 +- src/components/LHNOptionsList/OptionRowLHN.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 444c03108e962..854c187a9fa56 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -102,7 +102,7 @@ function Badge({ accessible={false} > {!!icon && ( - + From 92b342755a1941af284e8c1f66bb48e724331874 Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 16:00:32 +0000 Subject: [PATCH 09/11] add translations --- src/languages/de.ts | 6 ++++++ src/languages/es.ts | 6 ++++++ src/languages/fr.ts | 6 ++++++ src/languages/it.ts | 6 ++++++ src/languages/ja.ts | 6 ++++++ src/languages/nl.ts | 6 ++++++ src/languages/pl.ts | 6 ++++++ src/languages/pt-BR.ts | 6 ++++++ src/languages/zh-hans.ts | 6 ++++++ 9 files changed, 54 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index cf89470128081..a5c8ab5cb6d43 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Startseite', inbox: 'Posteingang', yourReviewIsRequired: 'Ihre Überprüfung ist erforderlich', + actionBadge: { + submit: 'Senden', + approve: 'Genehmigen', + pay: 'Bezahlen', + fix: 'Beheben', + }, success: 'Erfolgreich', group: 'Gruppe', profile: 'Profil', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8f2cc5d174ff4..cd4237ba67668 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -65,6 +65,12 @@ const translations: TranslationDeepObject = { workspaces: 'Espacios de trabajo', inbox: 'Recibidos', yourReviewIsRequired: 'Se requiere tu revisión', + actionBadge: { + submit: 'Enviar', + approve: 'Aprobar', + pay: 'Pagar', + fix: 'Corregir', + }, home: 'Inicio', group: 'Grupo', profile: 'Perfil', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 90d1bc29ecdc5..5ec3d8e7170dc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Accueil', inbox: 'Boîte de réception', yourReviewIsRequired: 'Votre révision est requise', + actionBadge: { + submit: 'Soumettre', + approve: 'Approuver', + pay: 'Payer', + fix: 'Corriger', + }, success: 'Réussi', group: 'Groupe', profile: 'Profil', diff --git a/src/languages/it.ts b/src/languages/it.ts index f8387f93a6ad9..a632c2bc0a2ef 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Home', inbox: 'Posta in arrivo', yourReviewIsRequired: 'È richiesta la tua revisione', + actionBadge: { + submit: 'Invia', + approve: 'Approva', + pay: 'Paga', + fix: 'Correggi', + }, success: 'Operazione riuscita', group: 'Gruppo', profile: 'Profilo', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b0d71e2cad750..bee8959d66d84 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'ホーム', inbox: '受信トレイ', yourReviewIsRequired: '確認が必要です', + actionBadge: { + submit: '送信', + approve: '承認する', + pay: '支払う', + fix: '修正', + }, success: '成功しました', group: 'グループ', profile: 'プロフィール', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 19e934b4ad176..b6cc653252d34 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Home', inbox: 'Inbox', yourReviewIsRequired: 'Uw beoordeling is vereist', + actionBadge: { + submit: 'Verzenden', + approve: 'Goedkeuren', + pay: 'Betalen', + fix: 'Oplossen', + }, success: 'Gelukt', group: 'Groep', profile: 'Profiel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 280523cb00958..28cd10d89451b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Strona główna', inbox: 'Skrzynka odbiorcza', yourReviewIsRequired: 'Wymagana jest Twoja weryfikacja', + actionBadge: { + submit: 'Wyślij', + approve: 'Zatwierdź', + pay: 'Zapłać', + fix: 'Napraw', + }, success: 'Sukces', group: 'Grupa', profile: 'Profil', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index ccc0a57f72d7f..f0e82a7e06f93 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: 'Início', inbox: 'Caixa de entrada', yourReviewIsRequired: 'Sua revisão é necessária', + actionBadge: { + submit: 'Enviar', + approve: 'Aprovar', + pay: 'Pagar', + fix: 'Corrigir', + }, success: 'Concluído', group: 'Grupo', profile: 'Perfil', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8a754ecc230bf..3fff31f2d594f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -192,6 +192,12 @@ const translations: TranslationDeepObject = { home: '主页', inbox: '收件箱', yourReviewIsRequired: '需要您的审核', + actionBadge: { + submit: '提交', + approve: '批准', + pay: '支付', + fix: '修复', + }, success: '成功', group: '群组', profile: '个人资料', From b4fe2d6c058ac849c309e61fbde3fa20aacfe93a Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 17:20:33 +0000 Subject: [PATCH 10/11] improve accessibility labels for badge --- src/components/LHNOptionsList/OptionRowLHN.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 755743ddb384a..1165c5da5e505 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -162,6 +162,12 @@ function OptionRowLHN({ const brickRoadIndicator = optionItem.brickRoadIndicator; const actionBadgeText = !isProduction && optionItem.actionBadge ? translate(`common.actionBadge.${optionItem.actionBadge}`) : ''; + let accessibilityLabelForBadge = ''; + if (brickRoadIndicator) { + accessibilityLabelForBadge = `. ${translate('common.yourReviewIsRequired')}, ${actionBadgeText}`; + } else if (optionItem.isPinned) { + accessibilityLabelForBadge = `. ${translate('common.pinned')}`; + } const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = shouldUseBoldText(optionItem) ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, styles.flexShrink0, style]; @@ -297,7 +303,7 @@ function OptionRowLHN({ (hovered || isContextMenuActive) && !isOptionFocused ? styles.sidebarLinkHover : null, ]} role={CONST.ROLE.BUTTON} - accessibilityLabel={`${translate('accessibilityHints.navigatesToChat')} ${optionItem.text}. ${optionItem.isUnread ? `${translate('common.unread')}.` : ''} ${optionItem.alternateText}${brickRoadIndicator ? `. ${translate('common.yourReviewIsRequired')}` : ''}`} + accessibilityLabel={`${translate('accessibilityHints.navigatesToChat')} ${optionItem.text}. ${optionItem.isUnread ? `${translate('common.unread')}.` : ''} ${optionItem.alternateText}${accessibilityLabelForBadge}`} onLayout={onLayout} needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} sentryLabel={CONST.SENTRY_LABEL.LHN.OPTION_ROW} From 85516e74145bd795b4a6e59e4c1f3c8589069697 Mon Sep 17 00:00:00 2001 From: aimane-chnaif Date: Thu, 12 Mar 2026 18:02:25 +0000 Subject: [PATCH 11/11] revert badge wrapper style --- src/components/Badge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 854c187a9fa56..855f2805908e8 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -85,11 +85,11 @@ function Badge({ styles.defaultBadge, isCondensed && styles.condensedBadge, styles.alignSelfCenter, - styles.ml1, + styles.ml2, StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC, isStrong), badgeStyles, ], - [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml1, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong], + [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml2, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong], ); return (