Skip to content

Commit dc1fd12

Browse files
Merge remote-tracking branch 'origin/main' into cm-stardate-card-util
Co-authored-by: Carlos Miceli <carlosmiceli@users.noreply.github.com>
2 parents df3389b + db4fb46 commit dc1fd12

11 files changed

Lines changed: 111 additions & 27 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/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) {
4646

4747
const pan = Gesture.Pan()
4848
.runOnJS(true)
49+
// Reduce gesture threshold so quick taps trigger onFinalize on iOS.
50+
.minDistance(0)
51+
.activateAfterLongPress(0)
4952
.onBegin((event) => {
5053
setIsSliderPressed(true);
5154
checkIfVideoIsPlaying(onCheckIfVideoIsPlaying);

src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) {
2626
const updatePlaybackSpeed = useCallback(
2727
(speed: PlaybackSpeed) => {
2828
setCurrentPlaybackSpeed(speed);
29-
if (!videoPopoverMenuPlayerRef.current) {
29+
30+
// We check if the player ref exists and if the playback rate is already set to the new speed to avoid redundant updates.
31+
// On iOS, setting the playback rate can cause the video to resume playback if it was paused.
32+
if (!videoPopoverMenuPlayerRef.current || videoPopoverMenuPlayerRef.current.playbackRate === speed) {
3033
return;
3134
}
3235
videoPopoverMenuPlayerRef.current.playbackRate = speed;

src/languages/de.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ import type {
6565
UpdatedPolicyBudgetNotificationParams,
6666
UpdatedPolicyCategoriesParams,
6767
UpdatedPolicyCategoryMaxAmountNoReceiptParams,
68+
UpdatedPolicyCurrencyDefaultTaxParams,
69+
UpdatedPolicyCustomTaxNameParams,
6870
UpdatedPolicyCustomUnitSubRateParams,
6971
UpdatedPolicyDefaultTitleParams,
72+
UpdatedPolicyForeignCurrencyDefaultTaxParams,
7073
UpdatedPolicyManualApprovalThresholdParams,
7174
UpdatedPolicyOwnershipParams,
7275
UpdatedPolicyPreventSelfApprovalParams,
@@ -76,9 +79,6 @@ import type {
7679
UpdatedPolicyReportFieldDefaultValueParams,
7780
UpdatedPolicyTagFieldParams,
7881
UpdatedPolicyTagListParams,
79-
UpdatedPolicyCurrencyDefaultTaxParams,
80-
UpdatedPolicyCustomTaxNameParams,
81-
UpdatedPolicyForeignCurrencyDefaultTaxParams,
8282
UpdatedPolicyTagListRequiredParams,
8383
UpdatedPolicyTagNameParams,
8484
UpdatedPolicyTagParams,

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/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';
@@ -58,6 +58,7 @@ function DomainMembersSettingsPage({route}: DomainMembersSettingsPageProps) {
5858

5959
if (!value && account?.requiresTwoFactorAuth) {
6060
clearToggleTwoFactorAuthRequiredForDomainError(domainAccountID);
61+
clearValidateDomainTwoFactorCodeError();
6162
Navigation.navigate(ROUTES.DOMAIN_MEMBERS_SETTINGS_TWO_FACTOR_AUTH.getRoute(domainAccountID));
6263
} else {
6364
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';
@@ -35,14 +35,30 @@ function DomainRequireTwoFactorAuthPage({route}: DomainRequireTwoFactorAuthPageP
3535
canBeMissing: false,
3636
selector: domainMemberSettingsSelector,
3737
});
38-
const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {
38+
const [validateDomainTwoFactorCodeErrors] = useOnyx(ONYXKEYS.VALIDATE_DOMAIN_TWO_FACTOR_CODE, {
3939
canBeMissing: true,
4040
});
4141
const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, {
4242
canBeMissing: true,
4343
});
4444

4545
const baseTwoFactorAuthRef = useRef<BaseTwoFactorAuthFormRef>(null);
46+
const isUnmounted = useRef(false);
47+
48+
useEffect(() => {
49+
return () => {
50+
isUnmounted.current = true;
51+
};
52+
}, []);
53+
54+
useEffect(() => {
55+
return () => {
56+
if (!isUnmounted.current) {
57+
return;
58+
}
59+
clearValidateDomainTwoFactorCodeError();
60+
};
61+
}, []);
4662

4763
useEffect(() => {
4864
if (domainSettings?.twoFactorAuthRequired) {
@@ -84,12 +100,12 @@ function DomainRequireTwoFactorAuthPage({route}: DomainRequireTwoFactorAuthPageP
84100
}}
85101
shouldAutoFocus={false}
86102
onInputChange={() => {
87-
if (isEmptyObject(domainErrors?.setTwoFactorAuthRequiredError)) {
103+
if (isEmptyObject(validateDomainTwoFactorCodeErrors?.errors)) {
88104
return;
89105
}
90-
clearToggleTwoFactorAuthRequiredForDomainError(domainAccountID);
106+
clearValidateDomainTwoFactorCodeError();
91107
}}
92-
errorMessage={getLatestErrorMessage({errors: domainErrors?.setTwoFactorAuthRequiredError})}
108+
errorMessage={getLatestErrorMessage({errors: validateDomainTwoFactorCodeErrors?.errors})}
93109
/>
94110
</View>
95111
</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:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type * as OnyxCommon from './OnyxCommon';
2+
3+
/** Server side errors from 2FA code validation in domain 2FA settings */
4+
type ValidateDomainTwoFactorCode = {
5+
/** Errors keyed by microtime */
6+
errors?: OnyxCommon.Errors;
7+
};
8+
9+
export default ValidateDomainTwoFactorCode;

0 commit comments

Comments
 (0)