diff --git a/babel.config.js b/babel.config.js
index 3fe49c3e034b..dd62684232c8 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -32,8 +32,6 @@ const webpack = {
const metro = {
presets: [require('metro-react-native-babel-preset')],
plugins: [
- 'react-native-reanimated/plugin',
-
// This is needed due to a react-native bug: https://github.com/facebook/react-native/issues/29084#issuecomment-1030732709
// It is included in metro-react-native-babel-preset but needs to be before plugin-proposal-class-properties or FlatList will break
'@babel/plugin-transform-flow-strip-types',
@@ -41,6 +39,8 @@ const metro = {
['@babel/plugin-proposal-class-properties', {loose: true}],
['@babel/plugin-proposal-private-methods', {loose: true}],
['@babel/plugin-proposal-private-property-in-object', {loose: true}],
+ // The reanimated babel plugin needs to be last, as stated here: https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/installation
+ 'react-native-reanimated/plugin',
],
};
diff --git a/contributingGuides/STYLING.md b/contributingGuides/STYLING.md
index ea402f162373..3842d983725c 100644
--- a/contributingGuides/STYLING.md
+++ b/contributingGuides/STYLING.md
@@ -2,7 +2,11 @@
## Where to Define Styles
-All styles must be defined in the `/styles` directory and `styles.js` contains the final export after gathering all appropriate styles. Unlike some React Native applications we are not using `StyleSheet.create()` and instead store styles as plain JS objects. There are also many helper styles available for direct use in components.
+Styles can either be theme-related or not. "Theme-related" means that a style contains some sort of color attributes (backgroundColor, color, borderColor). "Non-theme-related" styles may not contain no color attributes.
+
+All non-theme-related styles must be defined in the `/styles` directory and `styles.js` contains the final export after gathering all appropriate styles. Unlike some React Native applications we are not using `StyleSheet.create()` and instead store styles as plain JS objects. There are also many helper styles available for direct use in components.
+
+All styles that contain theme colors have to be defined in the `ThemeStylesProvider` component, as those need to be dynamically created and animated.
These helper styles are loosely based on the [Bootstrap system of CSS utility helper classes](https://getbootstrap.com/docs/5.0/utilities/spacing/) and are typically incremented by units of `4`.
diff --git a/src/App.js b/src/App.js
index 180b44d3c6a4..c02c7b2fe2a3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -17,6 +17,8 @@ import SafeArea from './components/SafeArea';
import * as Environment from './libs/Environment/Environment';
import {WindowDimensionsProvider} from './components/withWindowDimensions';
import {KeyboardStateProvider} from './components/withKeyboardState';
+import ThemeProvider from './styles/themes/ThemeProvider';
+import ThemeStylesProvider from './styles/ThemeStylesProvider';
import {CurrentReportIDContextProvider} from './components/withCurrentReportID';
import * as Session from './libs/actions/Session';
@@ -50,6 +52,8 @@ function App() {
KeyboardStateProvider,
CurrentReportIDContextProvider,
PickerStateProvider,
+ ThemeProvider,
+ ThemeStylesProvider,
]}
>
diff --git a/src/components/OnyxProvider.js b/src/components/OnyxProvider.js
index 1c3f6732e88d..380328cf8137 100644
--- a/src/components/OnyxProvider.js
+++ b/src/components/OnyxProvider.js
@@ -13,6 +13,7 @@ const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext
const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
+const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
const propTypes = {
/** Rendered child component */
@@ -30,6 +31,7 @@ function OnyxProvider(props) {
BlockedFromConciergeProvider,
BetasProvider,
ReportCommentDraftsProvider,
+ PreferredThemeProvider,
]}
>
{props.children}
@@ -42,4 +44,16 @@ OnyxProvider.propTypes = propTypes;
export default OnyxProvider;
-export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext, BetasContext, withReportCommentDrafts};
+export {
+ withNetwork,
+ withPersonalDetails,
+ withReportActionsDrafts,
+ withCurrentDate,
+ withBlockedFromConcierge,
+ withBetas,
+ NetworkContext,
+ BetasContext,
+ withReportCommentDrafts,
+ withPreferredTheme,
+ PreferredThemeContext,
+};
diff --git a/src/styles/ThemeStylesContext.js b/src/styles/ThemeStylesContext.js
new file mode 100644
index 000000000000..1c81ab3b39a5
--- /dev/null
+++ b/src/styles/ThemeStylesContext.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import styles from './styles';
+
+const ThemeStylesContext = React.createContext(styles);
+
+export default ThemeStylesContext;
diff --git a/src/styles/ThemeStylesProvider.js b/src/styles/ThemeStylesProvider.js
new file mode 100644
index 000000000000..4653605fc079
--- /dev/null
+++ b/src/styles/ThemeStylesProvider.js
@@ -0,0 +1,37 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React, {useMemo} from 'react';
+import PropTypes from 'prop-types';
+import useTheme from './themes/useTheme';
+import StylesContext from './ThemeStylesContext';
+import defaultStyles from './styles';
+
+const propTypes = {
+ /** Rendered child component */
+ children: PropTypes.node.isRequired,
+};
+
+function ThemeStylesProvider(props) {
+ const theme = useTheme();
+
+ const appContentStyle = useMemo(
+ () => ({
+ ...defaultStyles.appContent,
+ backgroundColor: theme.appBG,
+ }),
+ [theme.appBG],
+ );
+
+ const styles = useMemo(
+ () => ({
+ ...defaultStyles,
+ appContent: appContentStyle,
+ }),
+ [appContentStyle],
+ );
+
+ return {props.children};
+}
+ThemeStylesProvider.propTypes = propTypes;
+ThemeStylesProvider.displayName = 'ThemeStylesProvider';
+
+export default ThemeStylesProvider;
diff --git a/src/styles/themes/ThemeContext.js b/src/styles/themes/ThemeContext.js
new file mode 100644
index 000000000000..30d476c22d9c
--- /dev/null
+++ b/src/styles/themes/ThemeContext.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import defaultColors from './default';
+
+const ThemeContext = React.createContext(defaultColors);
+
+export default ThemeContext;
diff --git a/src/styles/themes/ThemeProvider.js b/src/styles/themes/ThemeProvider.js
new file mode 100644
index 000000000000..f4601712a0bc
--- /dev/null
+++ b/src/styles/themes/ThemeProvider.js
@@ -0,0 +1,27 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React, {useMemo} from 'react';
+import PropTypes from 'prop-types';
+import ThemeContext from './ThemeContext';
+import useThemePreference from './useThemePreference';
+import CONST from '../../CONST';
+
+// Going to eventually import the light theme here too
+import darkTheme from './default';
+
+const propTypes = {
+ /** Rendered child component */
+ children: PropTypes.node.isRequired,
+};
+
+function ThemeProvider(props) {
+ const themePreference = useThemePreference();
+
+ const theme = useMemo(() => (themePreference === CONST.THEME.LIGHT ? /* TODO: replace with light theme */ darkTheme : darkTheme), [themePreference]);
+
+ return {props.children};
+}
+
+ThemeProvider.propTypes = propTypes;
+ThemeProvider.displayName = 'ThemeProvider';
+
+export default ThemeProvider;
diff --git a/src/styles/themes/useTheme.js b/src/styles/themes/useTheme.js
new file mode 100644
index 000000000000..8e88b23a7688
--- /dev/null
+++ b/src/styles/themes/useTheme.js
@@ -0,0 +1,14 @@
+import {useContext} from 'react';
+import ThemeContext from './ThemeContext';
+
+function useTheme() {
+ const theme = useContext(ThemeContext);
+
+ if (!theme) {
+ throw new Error('StylesContext was null! Are you sure that you wrapped the component under a ?');
+ }
+
+ return theme;
+}
+
+export default useTheme;
diff --git a/src/styles/themes/useThemePreference.js b/src/styles/themes/useThemePreference.js
new file mode 100644
index 000000000000..fbb557423f10
--- /dev/null
+++ b/src/styles/themes/useThemePreference.js
@@ -0,0 +1,28 @@
+import {useState, useEffect, useContext} from 'react';
+import {Appearance} from 'react-native';
+import CONST from '../../CONST';
+import {PreferredThemeContext} from '../../components/OnyxProvider';
+
+function useThemePreference() {
+ const [themePreference, setThemePreference] = useState(CONST.THEME.DEFAULT);
+ const [systemTheme, setSystemTheme] = useState();
+ const preferredThemeContext = useContext(PreferredThemeContext);
+
+ useEffect(() => {
+ // This is used for getting the system theme, that can be set in the OS's theme settings. This will always return either "light" or "dark" and will update automatically if the OS theme changes.
+ const systemThemeSubscription = Appearance.addChangeListener(({colorScheme}) => setSystemTheme(colorScheme));
+ return systemThemeSubscription.remove;
+ }, []);
+
+ useEffect(() => {
+ const theme = preferredThemeContext || CONST.THEME.DEFAULT;
+
+ // If the user chooses to use the device theme settings, we need to set the theme preference to the system theme
+ if (theme === CONST.THEME.SYSTEM) setThemePreference(systemTheme);
+ else setThemePreference(theme);
+ }, [preferredThemeContext, systemTheme]);
+
+ return themePreference;
+}
+
+export default useThemePreference;
diff --git a/src/styles/useThemeStyles.js b/src/styles/useThemeStyles.js
new file mode 100644
index 000000000000..77ee0edb6f95
--- /dev/null
+++ b/src/styles/useThemeStyles.js
@@ -0,0 +1,14 @@
+import {useContext} from 'react';
+import ThemeStylesContext from './ThemeStylesContext';
+
+function useThemeStyles() {
+ const themeStyles = useContext(ThemeStylesContext);
+
+ if (!themeStyles) {
+ throw new Error('StylesContext was null! Are you sure that you wrapped the component under a ?');
+ }
+
+ return themeStyles;
+}
+
+export default useThemeStyles;