diff --git a/app.json b/app.json
index 6e3296f..9cc4a86 100644
--- a/app.json
+++ b/app.json
@@ -4,6 +4,7 @@
"slug": "imexmobile",
"version": "1.8.0",
"scheme": "imex-mobile-scheme",
+ "userInterfaceStyle": "automatic",
"extra": {
"expover": "1",
"eas": {
diff --git a/app/_layout.tsx b/app/_layout.tsx
index a826b32..7d1a577 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -2,6 +2,11 @@ import { checkUserSession } from "@/redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "@/redux/user/user.selectors";
import { ApolloProvider } from "@apollo/client";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
+import {
+ DarkTheme,
+ DefaultTheme,
+ ThemeProvider,
+} from "@react-navigation/native";
import { Stack } from "expo-router";
import {
Icon,
@@ -9,7 +14,7 @@ import {
NativeTabs,
VectorIcon,
} from "expo-router/unstable-native-tabs";
-import { useEffect } from "react";
+import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Platform, View } from "react-native";
import { Provider as PaperProvider } from "react-native-paper";
@@ -17,45 +22,55 @@ import { connect, Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { createStructuredSelector } from "reselect";
import { client } from "../graphql/client";
+import { useTheme as usePaperTheme } from "../hooks/useTheme";
import { persistor, store } from "../redux/store";
import "../translations/i18n";
-import theme from "../util/theme";
function AuthenticatedLayout() {
const { t } = useTranslation();
+ const paperTheme = usePaperTheme();
return (
-
-
-
+
+
+
+
- {Platform.select({
- ios: ,
- android: (
- }
- />
- ),
- })}
-
-
- {Platform.select({
- ios: ,
- android: (
- } />
- ),
- })}
-
-
-
- {Platform.select({
- //ios: ,
- android: (
- } />
- ),
- })}
-
-
-
+ {Platform.select({
+ ios: ,
+ android: (
+ }
+ />
+ ),
+ })}
+
+
+ {Platform.select({
+ ios: ,
+ android: (
+ }
+ />
+ ),
+ })}
+
+
+
+ {Platform.select({
+ //ios: ,
+ android: (
+ } />
+ ),
+ })}
+
+
+
+
);
}
@@ -79,21 +94,38 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser,
});
-const mapDispatchToProps = (dispatch) => ({
+const mapDispatchToProps = (dispatch: any) => ({
checkUserSession: () => dispatch(checkUserSession()),
});
-function AppContent({ currentUser, checkUserSession, bodyshop }) {
+function AppContent({ currentUser, checkUserSession, bodyshop }: any) {
useEffect(() => {
checkUserSession();
- }, []);
+ }, [checkUserSession]);
if (currentUser.authorized === null) {
- return ;
+ return (
+
+
+
+ );
}
if (currentUser.authorized) {
- return ;
+ return (
+
+
+
+ );
}
- return ;
+ return (
+
+
+
+ );
+}
+
+function ThemedLayout({ children }: { children: React.ReactNode }) {
+ const themeToApply = usePaperTheme();
+ return {children};
}
const ConnectedAppContent = connect(
mapStateToProps,
@@ -105,9 +137,7 @@ export default function AppLayout() {
-
-
-
+
diff --git a/app/jobs/_layout.tsx b/app/jobs/_layout.tsx
index 72168bf..3c3123b 100644
--- a/app/jobs/_layout.tsx
+++ b/app/jobs/_layout.tsx
@@ -1,8 +1,7 @@
-import { Stack, useRouter } from "expo-router";
+import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";
function JobsStack() {
- const router = useRouter();
const { t } = useTranslation();
return (
{
- // router.setParams({
- // search: event?.nativeEvent?.text,
- // });
- // },
- // },
}}
/>
- {label}
+ {label}
{theContent}
);
diff --git a/components/error/error-display.jsx b/components/error/error-display.jsx
index e48076b..10563a6 100644
--- a/components/error/error-display.jsx
+++ b/components/error/error-display.jsx
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
-import { Text } from "react-native";
-import { Button, Card } from "react-native-paper";
+
+import { Button, Card, Text } from "react-native-paper";
export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
const { t } = useTranslation();
@@ -14,11 +14,11 @@ export default function ErrorDisplay({ errorMessage, error, onDismiss }) {
error ||
"An unknown error has occured."}
- {onDismiss ? (
-
-
-
- ) : null}
+ {onDismiss ? (
+
+
+
+ ) : null}
);
diff --git a/components/job-documents/job-documents.jsx b/components/job-documents/job-documents.jsx
index 200f1f9..ced9544 100644
--- a/components/job-documents/job-documents.jsx
+++ b/components/job-documents/job-documents.jsx
@@ -6,12 +6,11 @@ import {
FlatList,
Image,
RefreshControl,
- Text,
TouchableOpacity,
View,
} from "react-native";
import ImageView from "react-native-image-viewing";
-import { ActivityIndicator } from "react-native-paper";
+import { ActivityIndicator, Text } from "react-native-paper";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import env from "../../env";
diff --git a/components/job-notes/job-notes.jsx b/components/job-notes/job-notes.jsx
index 85d1fd4..e33a53e 100644
--- a/components/job-notes/job-notes.jsx
+++ b/components/job-notes/job-notes.jsx
@@ -5,8 +5,8 @@ import { useGlobalSearchParams } from "expo-router";
import { DateTime } from "luxon";
import React from "react";
import { useTranslation } from "react-i18next";
-import { FlatList, RefreshControl, Text, View } from "react-native";
-import { ActivityIndicator, Card } from "react-native-paper";
+import { FlatList, RefreshControl, View } from "react-native";
+import { ActivityIndicator, Card, Text } from "react-native-paper";
import ErrorDisplay from "../error/error-display";
export default function JobNotes() {
diff --git a/components/job-tombstone/job-tombstone.jsx b/components/job-tombstone/job-tombstone.jsx
index 226e0ce..de097b2 100644
--- a/components/job-tombstone/job-tombstone.jsx
+++ b/components/job-tombstone/job-tombstone.jsx
@@ -3,14 +3,8 @@ import { useQuery } from "@apollo/client";
import { useLocalSearchParams } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
-import {
- RefreshControl,
- ScrollView,
- StyleSheet,
- Text,
- View,
-} from "react-native";
-import { ActivityIndicator, Card, useTheme } from "react-native-paper";
+import { RefreshControl, ScrollView, StyleSheet, View } from "react-native";
+import { ActivityIndicator, Card, Text, useTheme } from "react-native-paper";
import DataLabelComponent from "../data-label/data-label";
export default function JobTombstone() {
@@ -82,14 +76,9 @@ export default function JobTombstone() {
/>
- {`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
- job.v_model_desc || ""
- }`}
- {job.v_vin}
-
- }
+ content={`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
+ job.v_model_desc || ""
+ } - ${job.v_vin}`}
/>
diff --git a/components/jobs-list/job-list-item.jsx b/components/jobs-list/job-list-item.jsx
index 68373ad..58ec802 100644
--- a/components/jobs-list/job-list-item.jsx
+++ b/components/jobs-list/job-list-item.jsx
@@ -58,9 +58,7 @@ function JobListItemComponent({ openImagePicker, item }) {
style={[
styles.glassCard,
{
- backgroundColor: theme.dark
- ? "rgba(30,30,30,0.55)"
- : "rgba(255,255,255,0.55)",
+ backgroundColor: theme.colors.primaryContainer,
borderColor: theme.colors.outlineVariant,
},
]}
diff --git a/components/settings/settings.jsx b/components/settings/settings.jsx
index d2d365e..8b6d3a2 100644
--- a/components/settings/settings.jsx
+++ b/components/settings/settings.jsx
@@ -11,6 +11,7 @@ import { Button, Card, Divider, List, Text } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
+import { ThemeSelector } from "../theme-selector/theme-selector";
import UploadDeleteSwitch from "./upload-delete-switch";
const mapStateToProps = createStructuredSelector({
@@ -24,6 +25,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(Tab);
function Tab({ bodyshop, currentUser }) {
const { t } = useTranslation();
+
const handleClearStorage = () => {
Alert.alert(
"Clear Local Cache",
@@ -47,7 +49,7 @@ function Tab({ bodyshop, currentUser }) {
};
return (
-
+
Settings
@@ -59,7 +61,6 @@ function Tab({ bodyshop, currentUser }) {
- {" "}
{t("mediabrowser.labels.deleteafterupload")}
@@ -97,6 +98,18 @@ function Tab({ bodyshop, currentUser }) {
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/theme-selector/theme-selector.tsx b/components/theme-selector/theme-selector.tsx
new file mode 100644
index 0000000..da1d5b4
--- /dev/null
+++ b/components/theme-selector/theme-selector.tsx
@@ -0,0 +1,37 @@
+import { setTheme } from "@/redux/user/user.actions";
+import { selectTheme } from "@/redux/user/user.selectors";
+import React from "react";
+import { SegmentedButtons } from "react-native-paper";
+import { useDispatch, useSelector } from "react-redux";
+
+/**
+ * Example component showing how to use the useTheme hook
+ * and how to dispatch theme changes
+ */
+export const ThemeSelector = () => {
+ const dispatch = useDispatch();
+
+ const currentTheme = useSelector(selectTheme);
+
+ const handleThemeChange = (theme: "light" | "dark" | "system") => {
+ dispatch(setTheme(theme));
+ };
+
+ return (
+
+ );
+};
diff --git a/components/upload-progress/upload-progress.jsx b/components/upload-progress/upload-progress.jsx
index 33eaaec..e0ee54f 100644
--- a/components/upload-progress/upload-progress.jsx
+++ b/components/upload-progress/upload-progress.jsx
@@ -1,5 +1,4 @@
import { clearUploadError } from "@/redux/photos/photos.actions";
-import theme from "@/util/theme";
import { formatBytes } from "@/util/uploadUtils";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
@@ -92,7 +91,7 @@ const styles = StyleSheet.create({
display: "flex",
marginLeft: 12,
marginRight: 12,
- backgroundColor: theme.colors.elevation.level3,
+ //backgroundColor: theme.colors.elevation.level3,
borderRadius: 20,
paddingTop: 12,
shadowColor: "#000",
diff --git a/hooks/index.ts b/hooks/index.ts
new file mode 100644
index 0000000..e1fd4b3
--- /dev/null
+++ b/hooks/index.ts
@@ -0,0 +1 @@
+export { useTheme } from "./useTheme";
diff --git a/hooks/use-color-scheme.ts b/hooks/use-color-scheme.ts
deleted file mode 100644
index 17e3c63..0000000
--- a/hooks/use-color-scheme.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { useColorScheme } from 'react-native';
diff --git a/hooks/use-color-scheme.web.ts b/hooks/use-color-scheme.web.ts
deleted file mode 100644
index 7eb1c1b..0000000
--- a/hooks/use-color-scheme.web.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useEffect, useState } from 'react';
-import { useColorScheme as useRNColorScheme } from 'react-native';
-
-/**
- * To support static rendering, this value needs to be re-calculated on the client side for web
- */
-export function useColorScheme() {
- const [hasHydrated, setHasHydrated] = useState(false);
-
- useEffect(() => {
- setHasHydrated(true);
- }, []);
-
- const colorScheme = useRNColorScheme();
-
- if (hasHydrated) {
- return colorScheme;
- }
-
- return 'light';
-}
diff --git a/hooks/use-theme-color.ts b/hooks/use-theme-color.ts
deleted file mode 100644
index 0cbc3a6..0000000
--- a/hooks/use-theme-color.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Learn more about light and dark modes:
- * https://docs.expo.dev/guides/color-schemes/
- */
-
-import { Colors } from '@/constants/theme';
-import { useColorScheme } from '@/hooks/use-color-scheme';
-
-export function useThemeColor(
- props: { light?: string; dark?: string },
- colorName: keyof typeof Colors.light & keyof typeof Colors.dark
-) {
- const theme = useColorScheme() ?? 'light';
- const colorFromProps = props[theme];
-
- if (colorFromProps) {
- return colorFromProps;
- } else {
- return Colors[theme][colorName];
- }
-}
diff --git a/hooks/useTheme.ts b/hooks/useTheme.ts
new file mode 100644
index 0000000..af1e28f
--- /dev/null
+++ b/hooks/useTheme.ts
@@ -0,0 +1,57 @@
+import { selectTheme } from "@/redux/user/user.selectors";
+import { darkTheme, lightTheme } from "@/util/theme";
+import { useColorScheme } from "react-native";
+import { useSelector } from "react-redux";
+
+type ThemePreference = "light" | "dark" | "system";
+
+/**
+ * Custom hook that returns the appropriate theme based on user preference
+ *
+ * This hook automatically selects the correct theme (light or dark) based on:
+ * - User's explicit preference (light/dark)
+ * - System appearance when "system" is selected
+ * - Defaults to system theme if no preference is set
+ *
+ * @returns The theme object compatible with React Native Paper
+ *
+ * @example
+ * ```typescript
+ * import { useTheme } from "@/hooks/useTheme";
+ *
+ * const MyComponent = () => {
+ * const theme = useTheme();
+ *
+ * return (
+ *
+ *
+ * Themed content
+ *
+ *
+ * );
+ * };
+ * ```
+ */
+export const useTheme = () => {
+ const userThemePreference: ThemePreference = useSelector(selectTheme);
+ const systemColorScheme = useColorScheme();
+
+ // Determine which theme to use based on user preference
+ let selectedTheme;
+
+ switch (userThemePreference) {
+ case "light":
+ selectedTheme = lightTheme;
+ break;
+ case "dark":
+ selectedTheme = darkTheme;
+ break;
+ case "system":
+ default:
+ // Use system preference when user selects "system" or as fallback
+ selectedTheme = systemColorScheme === "dark" ? darkTheme : lightTheme;
+ break;
+ }
+
+ return selectedTheme;
+};
\ No newline at end of file
diff --git a/redux/user/user.actions.js b/redux/user/user.actions.js
index ddd7c0f..b0b6e50 100644
--- a/redux/user/user.actions.js
+++ b/redux/user/user.actions.js
@@ -98,3 +98,7 @@ export const validatePasswordResetFailure = (error) => ({
type: UserActionTypes.VALIDATE_PASSWORD_RESET_FAILURE,
payload: error,
});
+export const setTheme = (theme) => ({
+ type: UserActionTypes.SET_THEME,
+ payload: theme,
+});
diff --git a/redux/user/user.reducer.js b/redux/user/user.reducer.js
index a5cf187..7c38c7b 100644
--- a/redux/user/user.reducer.js
+++ b/redux/user/user.reducer.js
@@ -7,6 +7,7 @@ const INITIAL_STATE = {
bodyshop: null,
signingIn: false,
error: null,
+ theme: "system"
};
const userReducer = (state = INITIAL_STATE, action) => {
@@ -48,6 +49,8 @@ const userReducer = (state = INITIAL_STATE, action) => {
case UserActionTypes.SET_SHOP_DETAILS:
return { ...state, bodyshop: action.payload };
+ case UserActionTypes.SET_THEME:
+ return { ...state, theme: action.payload };
case UserActionTypes.SIGN_IN_FAILURE:
case UserActionTypes.SIGN_OUT_FAILURE:
case UserActionTypes.EMAIL_SIGN_UP_FAILURE:
diff --git a/redux/user/user.selectors.js b/redux/user/user.selectors.js
index b6b762a..fa7a705 100644
--- a/redux/user/user.selectors.js
+++ b/redux/user/user.selectors.js
@@ -31,3 +31,8 @@ export const selectSigningIn = createSelector(
[selectUser],
(user) => user.signingIn
);
+
+export const selectTheme = createSelector(
+ [selectUser],
+ (user) => user.theme
+);
\ No newline at end of file
diff --git a/redux/user/user.types.js b/redux/user/user.types.js
index 0f5773c..fa46823 100644
--- a/redux/user/user.types.js
+++ b/redux/user/user.types.js
@@ -26,5 +26,6 @@ const UserActionTypes = {
VALIDATE_PASSWORD_RESET_START: "VALIDATE_PASSWORD_RESET_START",
VALIDATE_PASSWORD_RESET_SUCCESS: "VALIDATE_PASSWORD_RESET_SUCCESS",
VALIDATE_PASSWORD_RESET_FAILURE: "VALIDATE_PASSWORD_RESET_FAILURE",
+ SET_THEME: "SET_THEME",
};
export default UserActionTypes;
diff --git a/util/theme.js b/util/theme.js
index 27e31b7..107c28b 100644
--- a/util/theme.js
+++ b/util/theme.js
@@ -1,45 +1,92 @@
-export default //Custom values were used as the overrides did not work.
- {
- colors: {
- primary: "#1890ff",
- onPrimary: "#ffffff",
- primaryContainer: "#e1e1e1ff",
- onPrimaryContainer: "#001c3a",
- secondary: "#545f71",
- onSecondary: "#ffffff",
- secondaryContainer: "#d8e3f8",
- onSecondaryContainer: "#111c2b",
- tertiary: "#00658d",
- onTertiary: "#ffffff",
- tertiaryContainer: "#c6e7ff",
- onTertiaryContainer: "#001e2d",
- error: "#ba1a1a",
- onError: "#ffffff",
- errorContainer: "#ffdad6",
- onErrorContainer: "#410002",
- background: "#fdfcff",
- onBackground: "#1a1c1e",
- surface: "#fdfcff",
- onSurface: "#1a1c1e",
- surfaceVariant: "#dededeff",
- onSurfaceVariant: "#43474e",
- outline: "#74777f",
- outlineVariant: "#c3c6cf",
- shadow: "#000000",
- scrim: "#000000",
- inverseSurface: "#2f3033",
- inverseOnSurface: "#f1f0f4",
- inversePrimary: "#a5c8ff",
- elevation: {
- level0: "transparent",
- level1: "#f1f1f1ff",
- level2: "#e9eff9",
- level3: "#e1ebf6",
- level4: "#dfe9f5",
- level5: "#dae6f4",
- },
- surfaceDisabled: "rgba(26, 28, 30, 0.12)",
- onSurfaceDisabled: "rgba(26, 28, 30, 0.38)",
- backdrop: "rgba(45, 49, 56, 0.4)",
+const lightTheme = {
+ theme: 'light',
+ colors: {
+ primary: "#1890ff",
+ onPrimary: "#ffffff",
+ primaryContainer: "#e1e1e1ff",
+ onPrimaryContainer: "#001c3a",
+ secondary: "#545f71",
+ onSecondary: "#ffffff",
+ secondaryContainer: "#d8e3f8",
+ onSecondaryContainer: "#111c2b",
+ tertiary: "#00658d",
+ onTertiary: "#ffffff",
+ tertiaryContainer: "#c6e7ff",
+ onTertiaryContainer: "#001e2d",
+ error: "#ba1a1a",
+ onError: "#ffffff",
+ errorContainer: "#ffdad6",
+ onErrorContainer: "#410002",
+ background: "#fdfcff",
+ onBackground: "#1a1c1e",
+ surface: "#fdfcff",
+ onSurface: "#1a1c1e",
+ surfaceVariant: "#dededeff",
+ onSurfaceVariant: "#43474e",
+ outline: "#74777f",
+ outlineVariant: "#c3c6cf",
+ shadow: "#000000",
+ scrim: "#000000",
+ inverseSurface: "#2f3033",
+ inverseOnSurface: "#f1f0f4",
+ inversePrimary: "#a5c8ff",
+ elevation: {
+ level0: "transparent",
+ level1: "#f1f1f1ff",
+ level2: "#e9eff9",
+ level3: "#e1ebf6",
+ level4: "#dfe9f5",
+ level5: "#dae6f4",
},
- };
\ No newline at end of file
+ surfaceDisabled: "rgba(26, 28, 30, 0.12)",
+ onSurfaceDisabled: "rgba(26, 28, 30, 0.38)",
+ backdrop: "rgba(45, 49, 56, 0.4)",
+ },
+};
+
+const darkTheme = {
+ theme: 'dark',
+ colors: {
+ primary: "#a5c8ff",
+ onPrimary: "#001c3a",
+ primaryContainer: "#2c3e50",
+ onPrimaryContainer: "#d6e8ff",
+ secondary: "#bcc6db",
+ onSecondary: "#262f40",
+ secondaryContainer: "#3d4756",
+ onSecondaryContainer: "#d8e3f8",
+ tertiary: "#7dd3fc",
+ onTertiary: "#001e2d",
+ tertiaryContainer: "#004d66",
+ onTertiaryContainer: "#c6e7ff",
+ error: "#ffb4ab",
+ onError: "#690005",
+ errorContainer: "#93000a",
+ onErrorContainer: "#ffdad6",
+ background: "#0f1419",
+ onBackground: "#e4e1e6",
+ surface: "#0f1419",
+ onSurface: "#e4e1e6",
+ surfaceVariant: "#43474e",
+ onSurfaceVariant: "#c3c6cf",
+ outline: "#8d9199",
+ outlineVariant: "#43474e",
+ shadow: "#000000",
+ scrim: "#000000",
+ inverseSurface: "#e4e1e6",
+ inverseOnSurface: "#2f3033",
+ inversePrimary: "#1890ff",
+ elevation: {
+ level0: "transparent",
+ level1: "#1a1f2e",
+ level2: "#212837",
+ level3: "#293141",
+ level4: "#2b3344",
+ level5: "#2e3748",
+ },
+ surfaceDisabled: "rgba(228, 225, 230, 0.12)",
+ onSurfaceDisabled: "rgba(228, 225, 230, 0.38)",
+ backdrop: "rgba(45, 49, 56, 0.4)",
+ },
+};
+export { darkTheme, lightTheme };