diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2bcf9e3f036d..6287b454ce1e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -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', @@ -1525,6 +1529,7 @@ type OnyxValuesMapping = { [ONYXKEYS.DOMAIN_MEMBERS_SELECTED_FOR_MOVE]: string[]; [ONYXKEYS.VERIFY_3DS_SUBSCRIPTION]: string; [ONYXKEYS.PREFERRED_THEME]: ValueOf; + [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; diff --git a/src/components/HighContrastModeSwitcher.tsx b/src/components/HighContrastModeSwitcher.tsx new file mode 100644 index 000000000000..aa4a409b1e01 --- /dev/null +++ b/src/components/HighContrastModeSwitcher.tsx @@ -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 ( + + + + {translate('themePage.enableHighContrast')} + + + ); +} + +export default HighContrastModeSwitcher; diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx index 6f0a394ec728..fad79a147075 100644 --- a/src/components/LocalePicker.tsx +++ b/src/components/LocalePicker.tsx @@ -40,7 +40,7 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) { const shouldDisablePicker = AccountUtils.isValidateCodeFormSubmitting(account); return ( - + - + diff --git a/src/languages/de.ts b/src/languages/de.ts index d667dfc8f8e3..c8aaa46d395f 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -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: { diff --git a/src/languages/en.ts b/src/languages/en.ts index ef8b6dc4e5a5..90d64bcbfc58 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -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: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 124f418aa5ba..068047ce5603 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -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: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index ad9c07769040..27b79ed88ec1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -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: { diff --git a/src/languages/it.ts b/src/languages/it.ts index a6b6451b56b4..fd43d0d2b92f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -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: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 6f7b222188dc..7868e0d604bf 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2896,6 +2896,7 @@ ${date} の ${merchant} への ${amount}`, }, }, highContrastMode: 'ハイコントラストモード', + enableHighContrast: 'ハイコントラストを有効にする', chooseThemeBelowOrSync: '以下からテーマを選択するか、デバイスの設定と同期してください。', }, termsOfUse: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 8c3fcff4afbf..c7ccbc142956 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -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: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 166583676332..df058b870b84 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -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: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index e99cb41e547e..3a8a7d9f8b0b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -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: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 9743c91cd4c9..d2c6746053a6 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2839,6 +2839,7 @@ ${amount},商户:${merchant} - 日期:${date}`, }, }, highContrastMode: '高对比度模式', + enableHighContrast: '启用高对比度', chooseThemeBelowOrSync: '请选择下方的主题,或与您的设备设置同步。', }, termsOfUse: { diff --git a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx index ab7d2acd6208..1bb0cfa128cd 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreensInitHandler.tsx @@ -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'; @@ -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(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); @@ -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(() => { + 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); + } + User.setHighContrastIntent(null); + }, [isLoadingApp, highContrastIntent, preferredTheme, session?.authToken]); + return null; } diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 098ebb3dd54e..a5976c90ae09 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -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, diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 90a829debb56..922d49d690ac 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -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}`> @@ -1214,6 +1223,12 @@ function setContactMethodAsDefault( } function updateTheme(theme: ValueOf, 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> = [ { onyxMethod: Onyx.METHOD.SET, @@ -1233,6 +1248,10 @@ function updateTheme(theme: ValueOf, shouldGoBack = true) { } } +function setHighContrastIntent(hasIntent: boolean | null) { + Onyx.set(ONYXKEYS.SIGN_IN_HIGH_CONTRAST_INTENT, hasIntent); +} + /** * Sets a custom status */ @@ -1922,6 +1941,7 @@ export { updateChatPriorityMode, setContactMethodAsDefault, updateTheme, + setHighContrastIntent, resetContactMethodValidateCodeSentState, updateCustomStatus, clearCustomStatus, diff --git a/src/pages/signin/Licenses.tsx b/src/pages/signin/Licenses.tsx index 5a6227b3a966..02bb53f45755 100644 --- a/src/pages/signin/Licenses.tsx +++ b/src/pages/signin/Licenses.tsx @@ -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(); @@ -17,8 +19,13 @@ function Licenses() { ${translate('termsOfUse.license')}`} /> - - + + + + + + + ); diff --git a/src/styles/variables.ts b/src/styles/variables.ts index b6cb93c6ec5d..8af77aca57a6 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -189,6 +189,7 @@ export default { signInHeroImageMobileWidth: 303, signInHeroImageTabletHeight: 324.01, signInHeroImageTabletWidth: 346, + signInLocalePickerWidth: 150, signInHeroImageDesktopHeight: 362.4, signInHeroImageDesktopWidth: 386.99, signInHeroBackgroundWidth: 2000,