Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d07653d
feat: high contrast feature
truph01 Mar 13, 2026
17bf415
fix: lint
truph01 Mar 13, 2026
9ae478c
fix: remove redundant file
truph01 Mar 13, 2026
ccaa0a5
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 16, 2026
66acabc
fix: update textLight color
truph01 Mar 16, 2026
2a8c795
fix: update bordersBold for composer
truph01 Mar 16, 2026
caecbb6
fix: apply AI suggestions
truph01 Mar 16, 2026
12694f9
fix: apply AI suggestion in useThemePreference.ts
truph01 Mar 16, 2026
81506c8
fix: remove useMemo
truph01 Mar 16, 2026
385d2c5
fix: apply AI suggestion and add test
truph01 Mar 16, 2026
23978e2
fix: prettier
truph01 Mar 16, 2026
4892937
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 16, 2026
08e66b5
fix: update iconColorfulBackground for contrast themes
truph01 Mar 16, 2026
b144c7f
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 17, 2026
008dc72
fix: remove updateThemeInPlace
truph01 Mar 18, 2026
3d007ad
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 19, 2026
786bfba
fix: search route border color
truph01 Mar 19, 2026
373cc9c
fix: ThemePage UI
truph01 Mar 19, 2026
ee42377
fix: conflicts
truph01 Mar 19, 2026
69241a9
fix: conflicts
truph01 Mar 19, 2026
8a64f78
fix: update buttonSuccessText and textLight in dark contrast
truph01 Mar 20, 2026
454713d
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 23, 2026
660d148
fix: update textSupporting: colors.productDark800
truph01 Mar 23, 2026
91bd7c0
Merge branch 'Expensify:main' into feat/79228
truph01 Mar 24, 2026
fe222c9
fix: update getEmojiReactionBubbleStyle background color
truph01 Mar 24, 2026
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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,9 @@ const CONST = {
DARK: 'dark',
LIGHT: 'light',
SYSTEM: 'system',
DARK_CONTRAST: 'dark-contrast',
LIGHT_CONTRAST: 'light-contrast',
SYSTEM_CONTRAST: 'system-contrast',
},
COLOR_SCHEME: {
LIGHT: 'light',
Expand Down
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ const ONYXKEYS = {
MY_DOMAIN_SECURITY_GROUPS: 'myDomainSecurityGroups',

// The theme setting set by the user in preferences.
// This can be either "light", "dark" or "system"
// This can be either "light", "dark", "system", "light-contrast", "dark-contrast" or "system-contrast"
PREFERRED_THEME: 'nvp_preferredTheme',

// Information about the onyx updates IDs that were received from the server
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ function Button({
primaryText
);

const defaultFill = success || danger ? theme.textLight : theme.icon;
const defaultFill = success || danger ? theme.textLight : theme.buttonIcon;

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (icon || shouldShowRightIcon) {
Expand Down
6 changes: 3 additions & 3 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
icon={hasError ? icons.DotIndicator : icon}
iconFill={hasError ? theme.danger : undefined}
iconHoverFill={hasError ? theme.danger : undefined}
iconRightFill={hasError ? theme.icon : undefined}
iconRightHoverFill={hasError ? theme.icon : undefined}
iconRightFill={hasError ? theme.buttonIcon : undefined}
iconRightHoverFill={hasError ? theme.buttonIcon : undefined}
sentryLabel={sentryLabel}
/>

Expand Down Expand Up @@ -231,7 +231,7 @@ function ButtonWithDropdownMenu<IValueType>({ref, ...props}: ButtonWithDropdownM
height={shouldUseShortForm ? variables.iconSizeExtraSmall : undefined}
src={icons.DownArrow}
additionalStyles={[...(shouldUseShortForm ? [styles.pRelative, styles.t0] : []), isMenuVisible ? styles.flipUpsideDown : undefined]}
fill={success ? theme.buttonSuccessText : theme.icon}
fill={success ? theme.buttonSuccessText : theme.buttonIcon}
testID="dropdown-arrow-icon"
/>
</View>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
}}
caretHidden={shouldHideInputCaret}
shouldShowOfflineMessage
wrapperStyle={{...styles.border, ...styles.alignItemsCenter}}
wrapperStyle={{...styles.searchRouterBorder, ...styles.alignItemsCenter}}
wrapperFocusedStyle={styles.borderColorFocus}
isSearchingForReports={!!isSearchingForReports}
selection={selection}
Expand Down
19 changes: 11 additions & 8 deletions src/hooks/useThemePreference.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {useMemo} from 'react';
import {useColorScheme} from 'react-native';
import type {ThemePreferenceWithoutSystem} from '@styles/theme/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import useOnyx from './useOnyx';

function useThemePreference() {
function useThemePreference(): ThemePreferenceWithoutSystem {
const [preferredThemeFromStorage] = useOnyx(ONYXKEYS.PREFERRED_THEME);
const systemTheme = useColorScheme();

const themePreference = useMemo(() => {
const theme = preferredThemeFromStorage ?? CONST.THEME.DEFAULT;
const theme = preferredThemeFromStorage ?? CONST.THEME.DEFAULT;

// If the user chooses to use the device theme settings, set the theme preference to the system theme
return theme === CONST.THEME.SYSTEM ? ((systemTheme ?? CONST.THEME.FALLBACK) as 'light' | 'dark') : theme;
}, [preferredThemeFromStorage, systemTheme]);
if (theme === CONST.THEME.SYSTEM) {
return systemTheme === 'dark' ? CONST.THEME.DARK : CONST.THEME.LIGHT;
}

return themePreference;
if (theme === CONST.THEME.SYSTEM_CONTRAST) {
return systemTheme === 'dark' ? CONST.THEME.DARK_CONTRAST : CONST.THEME.LIGHT_CONTRAST;
}

return theme as ThemePreferenceWithoutSystem;
}

export default useThemePreference;
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2672,6 +2672,7 @@ ${amount} für ${merchant} – ${date}`,
label: 'Geräteeinstellungen verwenden',
},
},
highContrastMode: 'Hoher Kontrast',
chooseThemeBelowOrSync: 'Wählen Sie unten ein Design aus oder synchronisieren Sie es mit den Einstellungen Ihres Geräts.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2723,6 +2723,7 @@ const translations = {
label: 'Use device settings',
},
},
highContrastMode: 'High contrast mode',
chooseThemeBelowOrSync: 'Choose a theme below, or sync with your device settings.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2583,6 +2583,7 @@ ${amount} para ${merchant} - ${date}`,
label: 'Utiliza los ajustes del dispositivo',
},
},
highContrastMode: 'Modo de alto contraste',
chooseThemeBelowOrSync: 'Elige un tema a continuación o sincronízalo con los ajustes de tu dispositivo.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2679,6 +2679,7 @@ ${amount} pour ${merchant} - ${date}`,
label: 'Utiliser les paramètres de l’appareil',
},
},
highContrastMode: 'Mode contraste élevé',
chooseThemeBelowOrSync: 'Choisissez un thème ci-dessous ou synchronisez avec les réglages de votre appareil.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2668,6 +2668,7 @@ ${amount} per ${merchant} - ${date}`,
label: 'Usa le impostazioni del dispositivo',
},
},
highContrastMode: 'Modalità alto contrasto',
chooseThemeBelowOrSync: 'Scegli un tema qui sotto o sincronizza con le impostazioni del tuo dispositivo.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,7 @@ ${date} の ${merchant} への ${amount}`,
label: 'デバイスの設定を使用',
},
},
highContrastMode: 'ハイコントラストモード',
chooseThemeBelowOrSync: '以下からテーマを選択するか、デバイスの設定と同期してください。',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2666,6 +2666,7 @@ ${amount} voor ${merchant} - ${date}`,
label: 'Apparaatinstellingen gebruiken',
},
},
highContrastMode: 'Hoog contrast',
chooseThemeBelowOrSync: 'Kies hieronder een thema, of synchroniseer met de instellingen van je apparaat.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2660,6 +2660,7 @@ ${amount} dla ${merchant} - ${date}`,
label: 'Użyj ustawień urządzenia',
},
},
highContrastMode: 'Tryb wysokiego kontrastu',
chooseThemeBelowOrSync: 'Wybierz motyw poniżej lub zsynchronizuj z ustawieniami urządzenia.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,7 @@ ${amount} para ${merchant} - ${date}`,
label: 'Usar configurações do dispositivo',
},
},
highContrastMode: 'Modo de alto contraste',
chooseThemeBelowOrSync: 'Escolha um tema abaixo ou sincronize com as configurações do seu dispositivo.',
},
termsOfUse: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
label: '使用设备设置',
},
},
highContrastMode: '高对比度模式',
chooseThemeBelowOrSync: '请选择下方的主题,或与您的设备设置同步。',
},
termsOfUse: {
Expand Down
3 changes: 2 additions & 1 deletion src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import FS from '@libs/Fullstory';
import Log from '@libs/Log';
import shouldOpenLastVisitedPath from '@libs/shouldOpenLastVisitedPath';
import {getPathFromURL} from '@libs/Url';
import {getBaseTheme} from '@styles/theme/utils';
import {updateLastVisitedPath} from '@userActions/App';
import {updateOnboardingLastVisitedPath} from '@userActions/Welcome';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -145,7 +146,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N

// https://reactnavigation.org/docs/themes
const navigationTheme = useMemo(() => {
const defaultNavigationTheme = themePreference === CONST.THEME.DARK ? DarkTheme : DefaultTheme;
const defaultNavigationTheme = getBaseTheme(themePreference) === CONST.THEME.DARK ? DarkTheme : DefaultTheme;

return {
...defaultNavigationTheme,
Expand Down
6 changes: 4 additions & 2 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@

let currentUserAccountID = -1;
let currentEmail = '';
Onyx.connect({

Check warning on line 69 in src/libs/actions/User.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserAccountID = value?.accountID ?? CONST.DEFAULT_NUMBER_ID;
Expand Down Expand Up @@ -1214,7 +1214,7 @@
Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo));
}

function updateTheme(theme: ValueOf<typeof CONST.THEME>) {
function updateTheme(theme: ValueOf<typeof CONST.THEME>, shouldGoBack = true) {
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.PREFERRED_THEME>> = [
{
onyxMethod: Onyx.METHOD.SET,
Expand All @@ -1229,7 +1229,9 @@

API.write(WRITE_COMMANDS.UPDATE_THEME, parameters, {optimisticData});

Navigation.goBack();
if (shouldGoBack) {
Navigation.goBack();
}
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/pages/settings/Preferences/PreferencesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import getPlatform from '@libs/getPlatform';
import type Platform from '@libs/getPlatform/types';
import Navigation from '@libs/Navigation/Navigation';
import colors from '@styles/theme/colors';
import {getBaseTheme} from '@styles/theme/utils';
import CONST from '@src/CONST';
import {isFullySupportedLocale, LOCALE_TO_LANGUAGE_STRING} from '@src/CONST/LOCALES';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -139,7 +140,7 @@ function PreferencesPage() {
/>
<MenuItemWithTopDescription
shouldShowRightIcon
title={translate(`themePage.themes.${preferredTheme ?? CONST.THEME.DEFAULT}.label`)}
title={translate(`themePage.themes.${getBaseTheme(preferredTheme ?? CONST.THEME.DEFAULT)}.label`)}
description={translate('themePage.theme')}
onPress={() => Navigation.navigate(ROUTES.SETTINGS_THEME)}
wrapperStyle={styles.sectionMenuItemTopDescription}
Expand Down
56 changes: 45 additions & 11 deletions src/pages/settings/Preferences/ThemePage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/ListItem/RadioListItem';
import type {ListItem} from '@components/SelectionList/ListItem/types';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {getBaseTheme, getContrastTheme, isHighContrastTheme} from '@styles/theme/utils';
import {updateTheme as updateThemeUserAction} from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -18,25 +21,37 @@ type ThemeEntry = ListItem & {
value: ValueOf<typeof CONST.THEME>;
};

const BASE_THEMES = [CONST.THEME.LIGHT, CONST.THEME.DARK, CONST.THEME.SYSTEM] as const;

function ThemePage() {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
const isOptionSelected = useRef(false);
const {DEFAULT, FALLBACK, ...themes} = CONST.THEME;
const localesToThemes = Object.values(themes).map((theme) => ({

const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
const isHighContrast = isHighContrastTheme(currentTheme);
const currentBaseTheme = getBaseTheme(currentTheme);

const localesToThemes = BASE_THEMES.map((theme) => ({
value: theme,
text: translate(`themePage.themes.${theme}.label`),
keyForList: theme,
isSelected: (preferredTheme ?? CONST.THEME.DEFAULT) === theme,
isSelected: currentBaseTheme === theme,
}));

const updateTheme = (selectedTheme: ThemeEntry) => {
if (isOptionSelected.current) {
return;
}
isOptionSelected.current = true;
updateThemeUserAction(selectedTheme.value);
const themeToStore = isHighContrast ? getContrastTheme(selectedTheme.value) : selectedTheme.value;
updateThemeUserAction(themeToStore);
};

const onToggleHighContrast = (enabled: boolean) => {
const newTheme = enabled ? getContrastTheme(currentBaseTheme) : currentBaseTheme;
updateThemeUserAction(newTheme, false);
};

return (
Expand All @@ -49,13 +64,32 @@ function ThemePage() {
onBackButtonPress={() => Navigation.goBack()}
/>
<Text style={[styles.mh5, styles.mv4]}>{translate('themePage.chooseThemeBelowOrSync')}</Text>
<SelectionList
data={localesToThemes}
ListItem={RadioListItem}
onSelectRow={updateTheme}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList}
/>
<View style={styles.flex1}>
<SelectionList
data={localesToThemes}
ListItem={RadioListItem}
onSelectRow={updateTheme}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList}
listFooterContent={
<>
<View style={[styles.mh5, styles.borderTop]} />
<View style={[styles.flexRow, styles.mh5, styles.mv4, styles.justifyContentBetween, styles.alignItemsCenter]}>
<View style={styles.flex4}>
<Text>{translate('themePage.highContrastMode')}</Text>
</View>
<View style={[styles.flex1, styles.alignItemsEnd]}>
<Switch
accessibilityLabel={translate('themePage.highContrastMode')}
isOn={isHighContrast}
onToggle={onToggleHighContrast}
/>
</View>
</View>
</>
}
/>
</View>
</ScreenWrapper>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/wallet/WalletStatementPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import addTrailingForwardSlash from '@libs/UrlUtils';
import type {WalletStatementNavigatorParamList} from '@navigation/types';
import {getBaseTheme} from '@styles/theme/utils';
import {generateStatementPDF} from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -45,7 +46,7 @@ function WalletStatementPage({route}: WalletStatementPageProps) {
const encryptedAuthToken = session?.encryptedAuthToken ?? '';
const baseURL = addTrailingForwardSlash(getOldDotURLFromEnvironment(environment));
const cachedFileName = yearMonth ? walletStatement?.[yearMonth] : undefined;
const url = `${baseURL}statement.php?period=${yearMonth}${themePreference === CONST.THEME.DARK ? '&isDarkMode=true' : ''}`;
const url = `${baseURL}statement.php?period=${yearMonth}${getBaseTheme(themePreference) === CONST.THEME.DARK ? '&isDarkMode=true' : ''}`;

// Dismiss if the yearMonth route param is missing, malformed, or in the future
useEffect(() => {
Expand Down
Loading
Loading