Skip to content

Commit 2f8904c

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/remove-canBeMissing-v2
2 parents 24534d2 + f114ab1 commit 2f8904c

15 files changed

Lines changed: 163 additions & 45 deletions

File tree

src/CONST/index.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8942,22 +8942,22 @@ const CONST = {
89428942
HOME: {
89438943
ANNOUNCEMENTS: [
89448944
{
8945-
title: 'Start the year with smarter spending, admin controls, and more.',
8946-
subtitle: 'Product update',
8947-
url: 'https://use.expensify.com/blog/expensify-january-2026-product-update',
8948-
publishedDate: '2026-01-28',
8945+
title: 'Expensify and Xero partner to support SMBs',
8946+
subtitle: 'Press Release',
8947+
url: 'https://www.businesswire.com/news/home/20260212641796/en/Expensify-and-Xero-Enhance-Partnership-to-Support-Small-Businesses',
8948+
publishedDate: '2026-02-12',
89498949
},
89508950
{
8951-
title: 'Our favorite features + final upgrades of the year',
8951+
title: 'New Home page & upgraded Insights analytics',
89528952
subtitle: 'Product update',
8953-
url: 'https://use.expensify.com/blog/expensify-2025-year-end-product-update',
8954-
publishedDate: '2025-12-22',
8953+
url: 'https://use.expensify.com/blog/expensify-home-and-insights-update',
8954+
publishedDate: '2026-02-18',
89558955
},
89568956
{
8957-
title: 'Uber for business + Expensify automates ride and meal receipts',
8957+
title: 'Smarter spend controls & Concierge anywhere',
89588958
subtitle: 'Product update',
8959-
url: 'https://use.expensify.com/blog/uber-for-business-and-expensify-integration-update',
8960-
publishedDate: '2025-12-01',
8959+
url: 'https://use.expensify.com/blog/expensify-february-2026-product-update',
8960+
publishedDate: '2026-02-19',
89618961
},
89628962
],
89638963
},

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ const ONYXKEYS = {
127127
/** Store the information of magic code */
128128
VALIDATE_ACTION_CODE: 'validateActionCode',
129129

130+
/** Stores 2FA code validation errors in domain 2FA settings */
131+
VALIDATE_DOMAIN_TWO_FACTOR_CODE: 'validateDomainTwoFactorCode',
132+
130133
/** A list of policies that a user can join */
131134
JOINABLE_POLICIES: 'joinablePolicies',
132135

@@ -1248,6 +1251,7 @@ type OnyxValuesMapping = {
12481251
[ONYXKEYS.LOGIN_LIST]: OnyxTypes.LoginList;
12491252
[ONYXKEYS.PENDING_CONTACT_ACTION]: OnyxTypes.PendingContactAction;
12501253
[ONYXKEYS.VALIDATE_ACTION_CODE]: OnyxTypes.ValidateMagicCodeAction;
1254+
[ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE]: OnyxTypes.ValidateDomainTwoFactorCode;
12511255
[ONYXKEYS.JOINABLE_POLICIES]: OnyxTypes.JoinablePolicies;
12521256
[ONYXKEYS.VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES]: OnyxTypes.ValidateUserAndGetAccessiblePolicies;
12531257
[ONYXKEYS.SESSION]: OnyxTypes.Session;

src/libs/actions/Domain.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,7 +1080,12 @@ function closeUserAccount(domainAccountID: number, domain: string, targetEmail:
10801080

10811081
function toggleTwoFactorAuthRequiredForDomain(domainAccountID: number, domainName: string, twoFactorAuthRequired: boolean, twoFactorAuthCode?: string) {
10821082
const optimisticData: Array<
1083-
OnyxUpdate<typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER | typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS | typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS>
1083+
OnyxUpdate<
1084+
| typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER
1085+
| typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS
1086+
| typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS
1087+
| typeof ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE
1088+
>
10841089
> = [
10851090
{
10861091
onyxMethod: Onyx.METHOD.MERGE,
@@ -1105,8 +1110,13 @@ function toggleTwoFactorAuthRequiredForDomain(domainAccountID: number, domainNam
11051110
setTwoFactorAuthRequiredError: null,
11061111
},
11071112
},
1113+
{
1114+
onyxMethod: Onyx.METHOD.SET,
1115+
key: ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE,
1116+
value: null,
1117+
},
11081118
];
1109-
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS | typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS>> = [
1119+
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS | typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS | typeof ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE>> = [
11101120
{
11111121
onyxMethod: Onyx.METHOD.MERGE,
11121122
key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
@@ -1121,9 +1131,19 @@ function toggleTwoFactorAuthRequiredForDomain(domainAccountID: number, domainNam
11211131
setTwoFactorAuthRequiredError: null,
11221132
},
11231133
},
1134+
{
1135+
onyxMethod: Onyx.METHOD.SET,
1136+
key: ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE,
1137+
value: null,
1138+
},
11241139
];
11251140
const failureData: Array<
1126-
OnyxUpdate<typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER | typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS | typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS>
1141+
OnyxUpdate<
1142+
| typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER
1143+
| typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS
1144+
| typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS
1145+
| typeof ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE
1146+
>
11271147
> = [
11281148
{
11291149
onyxMethod: Onyx.METHOD.MERGE,
@@ -1138,9 +1158,20 @@ function toggleTwoFactorAuthRequiredForDomain(domainAccountID: number, domainNam
11381158
onyxMethod: Onyx.METHOD.MERGE,
11391159
key: `${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`,
11401160
value: {
1141-
setTwoFactorAuthRequiredError: getMicroSecondOnyxErrorWithTranslationKey('domain.members.forceTwoFactorAuthError'),
1161+
setTwoFactorAuthRequiredError: twoFactorAuthCode ? null : getMicroSecondOnyxErrorWithTranslationKey('domain.members.forceTwoFactorAuthError'),
11421162
},
11431163
},
1164+
...(twoFactorAuthCode
1165+
? [
1166+
{
1167+
onyxMethod: Onyx.METHOD.MERGE,
1168+
key: ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE,
1169+
value: {
1170+
errors: getMicroSecondOnyxErrorWithTranslationKey('domain.members.forceTwoFactorAuthError'),
1171+
},
1172+
} as OnyxUpdate<typeof ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE>,
1173+
]
1174+
: []),
11441175
{
11451176
onyxMethod: Onyx.METHOD.MERGE,
11461177
key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`,
@@ -1166,6 +1197,10 @@ function clearToggleTwoFactorAuthRequiredForDomainError(domainAccountID: number)
11661197
});
11671198
}
11681199

1200+
function clearValidateDomainTwoFactorCodeError() {
1201+
Onyx.set(ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE, null);
1202+
}
1203+
11691204
function setDomainVacationDelegate(domainAccountID: number, domainMemberAccountID: number, creator: string, vacationer: string, delegate: string, vacationDelegate?: BaseVacationDelegate) {
11701205
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.DOMAIN | typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS | typeof ONYXKEYS.COLLECTION.DOMAIN_ERRORS>> = [
11711206
{
@@ -1424,6 +1459,7 @@ export {
14241459
closeUserAccount,
14251460
toggleTwoFactorAuthRequiredForDomain,
14261461
clearToggleTwoFactorAuthRequiredForDomainError,
1462+
clearValidateDomainTwoFactorCodeError,
14271463
setDomainVacationDelegate,
14281464
deleteDomainVacationDelegate,
14291465
clearVacationDelegateError,

src/libs/actions/Policy/Policy.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,19 +1173,18 @@ function setWorkspaceReimbursement({
11731173
API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData});
11741174
}
11751175

1176-
function leaveWorkspace(currentUserAccountID: number, policyID?: string) {
1177-
if (!policyID) {
1176+
function leaveWorkspace(currentUserAccountID: number, policy: OnyxEntry<Policy>) {
1177+
if (!policy) {
11781178
return;
11791179
}
1180-
const policy = deprecatedAllPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
1181-
const workspaceChats = ReportUtils.getAllWorkspaceReports(policyID);
1180+
const workspaceChats = ReportUtils.getAllWorkspaceReports(policy.id);
11821181

11831182
const optimisticData: Array<
11841183
OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS>
11851184
> = [
11861185
{
11871186
onyxMethod: Onyx.METHOD.MERGE,
1188-
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
1187+
key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`,
11891188
value: {
11901189
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
11911190
employeeList: {
@@ -1200,7 +1199,7 @@ function leaveWorkspace(currentUserAccountID: number, policyID?: string) {
12001199
const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.REPORT_METADATA | typeof ONYXKEYS.COLLECTION.REPORT>> = [
12011200
{
12021201
onyxMethod: Onyx.METHOD.MERGE,
1203-
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
1202+
key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`,
12041203
value: null,
12051204
},
12061205
];
@@ -1209,7 +1208,7 @@ function leaveWorkspace(currentUserAccountID: number, policyID?: string) {
12091208
> = [
12101209
{
12111210
onyxMethod: Onyx.METHOD.MERGE,
1212-
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
1211+
key: `${ONYXKEYS.COLLECTION.POLICY}${policy.id}`,
12131212
value: {
12141213
pendingAction: policy?.pendingAction ?? null,
12151214
employeeList: {
@@ -1317,7 +1316,7 @@ function leaveWorkspace(currentUserAccountID: number, policyID?: string) {
13171316
}
13181317

13191318
const params: LeavePolicyParams = {
1320-
policyID,
1319+
policyID: policy.id,
13211320
email: deprecatedSessionEmail,
13221321
};
13231322
API.write(WRITE_COMMANDS.LEAVE_POLICY, params, {optimisticData, successData, failureData});

src/pages/domain/Members/DomainMembersSettingsPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation
1414
import type {SettingsNavigatorParamList} from '@navigation/types';
1515
import BaseDomainSettingsPage from '@pages/domain/BaseDomainSettingsPage';
1616
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
17-
import {clearToggleTwoFactorAuthRequiredForDomainError, toggleTwoFactorAuthRequiredForDomain} from '@userActions/Domain';
17+
import {clearToggleTwoFactorAuthRequiredForDomainError, clearValidateDomainTwoFactorCodeError, toggleTwoFactorAuthRequiredForDomain} from '@userActions/Domain';
1818
import ONYXKEYS from '@src/ONYXKEYS';
1919
import ROUTES from '@src/ROUTES';
2020
import type SCREENS from '@src/SCREENS';
@@ -53,6 +53,7 @@ function DomainMembersSettingsPage({route}: DomainMembersSettingsPageProps) {
5353

5454
if (!value && account?.requiresTwoFactorAuth) {
5555
clearToggleTwoFactorAuthRequiredForDomainError(domainAccountID);
56+
clearValidateDomainTwoFactorCodeError();
5657
Navigation.navigate(ROUTES.DOMAIN_MEMBERS_SETTINGS_TWO_FACTOR_AUTH.getRoute(domainAccountID));
5758
} else {
5859
toggleTwoFactorAuthRequiredForDomain(domainAccountID, domainName, value);

src/pages/domain/Members/DomainRequireTwoFactorAuthPage.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Navigation from '@navigation/Navigation';
1616
import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
1717
import type {SettingsNavigatorParamList} from '@navigation/types';
1818
import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper';
19-
import {clearToggleTwoFactorAuthRequiredForDomainError, toggleTwoFactorAuthRequiredForDomain} from '@userActions/Domain';
19+
import {clearValidateDomainTwoFactorCodeError, toggleTwoFactorAuthRequiredForDomain} from '@userActions/Domain';
2020
import ONYXKEYS from '@src/ONYXKEYS';
2121
import ROUTES from '@src/ROUTES';
2222
import type SCREENS from '@src/SCREENS';
@@ -34,10 +34,26 @@ function DomainRequireTwoFactorAuthPage({route}: DomainRequireTwoFactorAuthPageP
3434
const [domainSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
3535
selector: domainMemberSettingsSelector,
3636
});
37-
const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`);
37+
const [validateDomainTwoFactorCodeErrors] = useOnyx(ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE);
3838
const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`);
3939

4040
const baseTwoFactorAuthRef = useRef<BaseTwoFactorAuthFormRef>(null);
41+
const isUnmounted = useRef(false);
42+
43+
useEffect(() => {
44+
return () => {
45+
isUnmounted.current = true;
46+
};
47+
}, []);
48+
49+
useEffect(() => {
50+
return () => {
51+
if (!isUnmounted.current) {
52+
return;
53+
}
54+
clearValidateDomainTwoFactorCodeError();
55+
};
56+
}, []);
4157

4258
useEffect(() => {
4359
if (domainSettings?.twoFactorAuthRequired) {
@@ -79,12 +95,12 @@ function DomainRequireTwoFactorAuthPage({route}: DomainRequireTwoFactorAuthPageP
7995
}}
8096
shouldAutoFocus={false}
8197
onInputChange={() => {
82-
if (isEmptyObject(domainErrors?.setTwoFactorAuthRequiredError)) {
98+
if (isEmptyObject(validateDomainTwoFactorCodeErrors?.errors)) {
8399
return;
84100
}
85-
clearToggleTwoFactorAuthRequiredForDomainError(domainAccountID);
101+
clearValidateDomainTwoFactorCodeError();
86102
}}
87-
errorMessage={getLatestErrorMessage({errors: domainErrors?.setTwoFactorAuthRequiredError})}
103+
errorMessage={getLatestErrorMessage({errors: validateDomainTwoFactorCodeErrors?.errors})}
88104
/>
89105
</View>
90106
</ScrollView>

src/pages/inbox/sidebar/SidebarLinksData.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,20 @@ function SidebarLinksData({insets}: SidebarLinksDataProps) {
3030
currentReportIDRef.current = currentReportID;
3131
const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []);
3232

33-
// IMPORTANT: Always end the telemetry navigation span for the Inbox tab when the screen gains focus.
34-
// This must handle both the initial mount and all subsequent Inbox tab visits,
35-
// as onLayout does not fire when navigating back to an already-mounted screen.
33+
// Guards against ending the span before the first layout has completed.
34+
const hasHadFirstLayout = useRef(false);
35+
const onLayout = useCallback(() => {
36+
hasHadFirstLayout.current = true;
37+
endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
38+
}, []);
39+
40+
// On re-visits, react-freeze serves the cached layout — onLayout never fires.
41+
// useFocusEffect fires on unfreeze, which is when the screen becomes visible.
3642
useFocusEffect(
3743
useCallback(() => {
44+
if (!hasHadFirstLayout.current) {
45+
return;
46+
}
3847
endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB);
3948
}, []),
4049
);
@@ -45,6 +54,7 @@ function SidebarLinksData({insets}: SidebarLinksDataProps) {
4554
collapsable={false}
4655
accessibilityLabel={translate('sidebarScreen.listOfChats')}
4756
style={[styles.flex1, styles.h100]}
57+
onLayout={onLayout}
4858
>
4959
<SidebarLinks
5060
// Forwarded props:

src/pages/workspace/WorkspaceOverviewPage.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,14 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa
274274
]);
275275

276276
const handleLeaveWorkspace = useCallback(() => {
277-
if (!policyID) {
277+
if (!policy) {
278278
return;
279279
}
280280

281-
leaveWorkspace(currentUserPersonalDetails.accountID, policyID);
281+
leaveWorkspace(currentUserPersonalDetails.accountID, policy);
282282
setIsLeaveModalOpen(false);
283283
goBackFromInvalidPolicy();
284-
}, [currentUserPersonalDetails.accountID, policyID]);
284+
}, [currentUserPersonalDetails.accountID, policy]);
285285

286286
const hideDeleteWorkspaceErrorModal = () => {
287287
setIsDeleteWorkspaceErrorModalOpen(false);

src/pages/workspace/WorkspacesListPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,11 @@ function WorkspacesListPage() {
246246
};
247247

248248
const confirmLeaveAndHideModal = () => {
249-
if (!policyIDToLeave) {
249+
if (!policyToLeave) {
250250
return;
251251
}
252252

253-
leaveWorkspace(currentUserPersonalDetails.accountID, policyIDToLeave);
253+
leaveWorkspace(currentUserPersonalDetails.accountID, policyToLeave);
254254
setIsLeaveModalOpen(false);
255255
};
256256

src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
7575
}, [loadPolicyCompanyCardsPage, isOffline]);
7676

7777
const loadPolicyCompanyCardsFeed = useCallback(() => {
78-
if (isLoading || !bankName || isFeedPending) {
78+
if (isLoading || !bankName || isFeedPending || isOffline) {
7979
return;
8080
}
8181

8282
openPolicyCompanyCardsFeed(domainOrWorkspaceAccountID, policyID, bankName, translate);
83-
}, [bankName, domainOrWorkspaceAccountID, isFeedPending, isLoading, policyID, translate]);
83+
}, [bankName, domainOrWorkspaceAccountID, isFeedPending, isLoading, policyID, translate, isOffline]);
8484

8585
useEffect(() => {
8686
loadPolicyCompanyCardsFeed();

0 commit comments

Comments
 (0)