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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,10 @@ const ONYXKEYS = {
// This can be either "light", "dark", "system", "light-contrast", "dark-contrast" or "system-contrast"
PREFERRED_THEME: 'nvp_preferredTheme',

// Client-only flag set when a logged-out user enables high contrast on the sign-in page.
// It is reconciled with the server's base theme right after sign-in, then cleared.
SIGN_IN_HIGH_CONTRAST_INTENT: 'signInHighContrastIntent',

// Information about the onyx updates IDs that were received from the server
ONYX_UPDATES_FROM_SERVER: 'onyxUpdatesFromServer',

Expand Down Expand Up @@ -1525,6 +1529,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.DOMAIN_MEMBERS_SELECTED_FOR_MOVE]: string[];
[ONYXKEYS.VERIFY_3DS_SUBSCRIPTION]: string;
[ONYXKEYS.PREFERRED_THEME]: ValueOf<typeof CONST.THEME>;
[ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT]: boolean;
[ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken;
[ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.AnyOnyxUpdatesFromServer;
[ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number;
Expand Down
57 changes: 57 additions & 0 deletions src/components/HighContrastModeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getBaseTheme, getContrastTheme, isHighContrastTheme} from '@styles/theme/utils';
import variables from '@styles/variables';
import {setHighContrastIntent, updateTheme} from '@userActions/User';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import Icon from './Icon';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
import Text from './Text';

function HighContrastModeSwitcher() {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
const icons = useMemoizedLazyExpensifyIcons(['Lightbulb']);

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

const toggleHighContrast = () => {
const baseTheme = getBaseTheme(currentTheme);
updateTheme(isHighContrast ? baseTheme : getContrastTheme(baseTheme), false);
setHighContrastIntent(!isHighContrast);
};

return (
<PressableWithFeedback
onPress={toggleHighContrast}
sentryLabel="HighContrastModeSwitcher-Toggle"
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('themePage.enableHighContrast')}
accessibilityState={{checked: isHighContrast}}
wrapperStyle={styles.flex1}
style={[styles.flexRow, styles.alignItemsCenter]}
>
<Icon
src={icons.Lightbulb}
fill={isHighContrast ? theme.text : theme.icon}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
accessibilityLabel={translate('themePage.enableHighContrast')}
/>
<View style={[styles.ml2, styles.flex1, styles.pickerContainer, styles.pickerContainerSmall]}>
<Text style={styles.textSmall}>{translate('themePage.enableHighContrast')}</Text>
</View>
</PressableWithFeedback>
);
}

export default HighContrastModeSwitcher;
6 changes: 3 additions & 3 deletions src/components/LocalePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) {
const shouldDisablePicker = AccountUtils.isValidateCodeFormSubmitting(account);

return (
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]}>
<Icon
src={icons.Globe}
fill={theme.icon}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
accessibilityLabel={translate('languagePage.language')}
/>
<View style={styles.ml2}>
<View style={[styles.ml2, styles.flex1]}>
<Picker
label={size === 'normal' ? translate('languagePage.language') : null}
accessibilityLabel={`${translate('common.select')} ${translate('languagePage.language')}`}
Expand All @@ -59,7 +59,7 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) {
shouldShowOnlyTextWhenDisabled={false}
size={size}
value={preferredLocale}
containerStyles={size === 'small' ? styles.pickerContainerSmall : {}}
containerStyles={size === 'small' ? [styles.pickerContainerSmall, styles.w100] : {}}
backgroundColor={theme.signInPage}
/>
</View>
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,7 @@ ${amount} für ${merchant} – ${date}`,
},
},
highContrastMode: 'Hoher Kontrast',
enableHighContrast: 'Hohen Kontrast aktivieren',
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 @@ -2995,6 +2995,7 @@ const translations = {
},
},
highContrastMode: 'High contrast mode',
enableHighContrast: 'Enable high contrast',
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 @@ -2803,6 +2803,7 @@ ${amount} para ${merchant} - ${date}`,
},
},
highContrastMode: 'Modo de alto contraste',
enableHighContrast: 'Activar 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 @@ -2932,6 +2932,7 @@ ${amount} pour ${merchant} - ${date}`,
},
},
highContrastMode: 'Mode contraste élevé',
enableHighContrast: 'Activer le 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 @@ -2920,6 +2920,7 @@ ${amount} per ${merchant} - ${date}`,
},
},
highContrastMode: 'Modalità alto contrasto',
enableHighContrast: 'Attiva 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 @@ -2896,6 +2896,7 @@ ${date} の ${merchant} への ${amount}`,
},
},
highContrastMode: 'ハイコントラストモード',
enableHighContrast: 'ハイコントラストを有効にする',
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 @@ -2917,6 +2917,7 @@ ${amount} voor ${merchant} - ${date}`,
},
},
highContrastMode: 'Hoog contrast',
enableHighContrast: 'Hoog contrast inschakelen',
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 @@ -2911,6 +2911,7 @@ ${amount} dla ${merchant} - ${date}`,
},
},
highContrastMode: 'Tryb wysokiego kontrastu',
enableHighContrast: 'Włącz wysoki kontrast',
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 @@ -2911,6 +2911,7 @@ ${amount} para ${merchant} - ${date}`,
},
},
highContrastMode: 'Modo de alto contraste',
enableHighContrast: 'Ativar 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 @@ -2839,6 +2839,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
},
},
highContrastMode: '高对比度模式',
enableHighContrast: '启用高对比度',
chooseThemeBelowOrSync: '请选择下方的主题,或与您的设备设置同步。',
},
termsOfUse: {
Expand Down
22 changes: 22 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {getReportIDFromLink} from '@libs/ReportUtils';
import * as SessionUtils from '@libs/SessionUtils';
import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
import {getSearchParamFromUrl} from '@libs/Url';
import {getBaseTheme, getContrastTheme} from '@styles/theme/utils';
import {openAgentsPage} from '@userActions/Agent';
import * as App from '@userActions/App';
import * as Download from '@userActions/Download';
Expand Down Expand Up @@ -60,6 +61,10 @@ function AuthScreensInitHandler() {
const hasActiveAdminPolicies = useHasActiveAdminPolicies();

const [session] = useOnyx(ONYXKEYS.SESSION);
const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
const [highContrastIntent] = useOnyx(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT);
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const wasLoadingApp = useRef<boolean | undefined>(undefined);
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
const [betas] = useOnyx(ONYXKEYS.BETAS);
const [initialLastUpdateIDAppliedToClient] = useOnyx(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT);
Expand Down Expand Up @@ -157,6 +162,23 @@ function AuthScreensInitHandler() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// When a logged-out user enabled high contrast on the sign-in page, apply it to whatever base theme
// the server returns once they sign in. OpenApp merges the server's nvp_preferredTheme before flipping
// IS_LOADING_APP back to false, so the true -> false edge is when the server base theme is available.
useEffect(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ CLEAN-REACT-PATTERNS-4 (docs)

The AuthScreensInitHandler component already aggregates multiple unrelated responsibilities (session management, Pusher initialization, navigation setup, app loading). This new useEffect for reconciling the high-contrast theme intent after sign-in is an unrelated concern that further adds to the mix. If the high-contrast reconciliation logic changes, it requires navigating through all the other unrelated effects in this component.

Extract this effect into a focused custom hook, for example:

function useReconcileHighContrastIntent() {
    const [preferredTheme] = useOnyx(ONYXKEYS.PREFERRED_THEME);
    const [highContrastIntent] = useOnyx(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT);
    const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
    const [session] = useOnyx(ONYXKEYS.SESSION);
    const wasLoadingApp = useRef<boolean | undefined>(undefined);

    useEffect(() => {
        const hasFinishedLoading = !!wasLoadingApp.current && !isLoadingApp;
        wasLoadingApp.current = isLoadingApp;
        if (!hasFinishedLoading || !highContrastIntent || !session?.authToken) {
            return;
        }
        const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
        const targetTheme = getContrastTheme(getBaseTheme(currentTheme));
        if (currentTheme !== targetTheme) {
            User.updateTheme(targetTheme, false);
        }
        Onyx.set(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT, null);
    }, [isLoadingApp, highContrastIntent, preferredTheme, session?.authToken]);
}

Then call useReconcileHighContrastIntent() inside AuthScreensInitHandler (or in a separate component if appropriate), keeping the concern isolated and independently testable.


Reviewed at: 7f87a7e | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@Eskalifer1 lemme know if you agree with this and I'll change it.

Because I think it's probably fine to keep it in this file

const hasFinishedLoading = !!wasLoadingApp.current && !isLoadingApp;
wasLoadingApp.current = isLoadingApp;
if (!hasFinishedLoading || !highContrastIntent || !session?.authToken) {
Comment thread
rushatgabhane marked this conversation as resolved.
return;
}
const currentTheme = preferredTheme ?? CONST.THEME.DEFAULT;
const targetTheme = getContrastTheme(getBaseTheme(currentTheme));
if (currentTheme !== targetTheme) {
User.updateTheme(targetTheme, false);
}
User.setHighContrastIntent(null);
}, [isLoadingApp, highContrastIntent, preferredTheme, session?.authToken]);

return null;
}

Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const KEYS_TO_PRESERVE: OnyxKey[] = [
ONYXKEYS.SESSION,
ONYXKEYS.NVP_TRY_FOCUS_MODE,
ONYXKEYS.PREFERRED_THEME,
ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT,
ONYXKEYS.NVP_PREFERRED_LOCALE,
ONYXKEYS.CREDENTIALS,
ONYXKEYS.PRESERVED_USER_SESSION,
Expand Down
20 changes: 20 additions & 0 deletions src/libs/actions/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ import {showReportActionNotification} from './Report';
import {resendValidateCode as sessionResendValidateCode} from './Session';
import redirectToSignIn from './SignInRedirect';

// `sessionAccountID` is only used in actions, not during render. So `Onyx.connectWithoutView` is appropriate.
let sessionAccountID: number | undefined;
Onyx.connectWithoutView({
key: ONYXKEYS.SESSION,
callback: (value) => {
sessionAccountID = value?.accountID;
},
});

type DomainOnyxUpdate =
| OnyxUpdate<`${typeof ONYXKEYS.COLLECTION.DOMAIN}${string}`>
| OnyxUpdate<`${typeof ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${string}`>
Expand Down Expand Up @@ -1214,6 +1223,12 @@ function setContactMethodAsDefault(
}

function updateTheme(theme: ValueOf<typeof CONST.THEME>, shouldGoBack = true) {
// When toggling high contrast from the sign-in page, the user is not signed in. So persist the preference locally only.
if (!sessionAccountID) {
Onyx.set(ONYXKEYS.PREFERRED_THEME, theme);
return;
}

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

function setHighContrastIntent(hasIntent: boolean | null) {
Onyx.set(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT, hasIntent);
}

/**
* Sets a custom status
*/
Expand Down Expand Up @@ -1922,6 +1941,7 @@ export {
updateChatPriorityMode,
setContactMethodAsDefault,
updateTheme,
setHighContrastIntent,
resetContactMethodValidateCodeSentState,
updateCustomStatus,
clearCustomStatus,
Expand Down
11 changes: 9 additions & 2 deletions src/pages/signin/Licenses.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React from 'react';
import {View} from 'react-native';
import HighContrastModeSwitcher from '@components/HighContrastModeSwitcher';
import LocalePicker from '@components/LocalePicker';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';

const currentYear = new Date().getFullYear();

Expand All @@ -17,8 +19,13 @@ function Licenses() {
<View style={[styles.renderHTML, styles.flexRow]}>
<RenderHTML html={`<muted-text-xs>${translate('termsOfUse.license')}</muted-text-xs>`} />
</View>
<View style={[styles.mt4, styles.alignItemsCenter, styles.mb2, styles.flexRow, styles.justifyContentBetween]}>
<LocalePicker size="small" />
<View style={[styles.mt4, styles.mb2, {maxWidth: variables.signInLocalePickerWidth}]}>
<View style={[styles.alignItemsCenter, styles.mb2, styles.flexRow]}>
<HighContrastModeSwitcher />
</View>
<View style={[styles.alignItemsCenter, styles.flexRow]}>
<LocalePicker size="small" />
</View>
</View>
</>
);
Expand Down
1 change: 1 addition & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export default {
signInHeroImageMobileWidth: 303,
signInHeroImageTabletHeight: 324.01,
signInHeroImageTabletWidth: 346,
signInLocalePickerWidth: 150,
signInHeroImageDesktopHeight: 362.4,
signInHeroImageDesktopWidth: 386.99,
signInHeroBackgroundWidth: 2000,
Expand Down
Loading