From b5613c6e1840589b3042232c324ae869d6c745d8 Mon Sep 17 00:00:00 2001 From: Jakob Stornig Date: Sun, 17 Dec 2023 19:24:33 +0100 Subject: [PATCH 1/5] feat: ThemeContext --- app/_layout.tsx | 7 ++- app/contexts/ThemeContext.tsx | 97 +++++++++++++++++++++++++++++++++++ package-lock.json | 31 +++++++++++ package.json | 1 + 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 app/contexts/ThemeContext.tsx diff --git a/app/_layout.tsx b/app/_layout.tsx index 6f79126..0a38cb4 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,7 +1,8 @@ import { Slot } from 'expo-router'; import React, { useEffect } from 'react'; -import { initDatabase } from '../services/database'; +import { addCategory, initDatabase } from '../services/database'; import { AuthProvider } from './contexts/AuthContext'; +import { ThemeProvider } from './contexts/ThemeContext'; export default function _layout() { @@ -12,7 +13,9 @@ export default function _layout() { console.log("layout called") return ( - + + + ) } \ No newline at end of file diff --git a/app/contexts/ThemeContext.tsx b/app/contexts/ThemeContext.tsx new file mode 100644 index 0000000..9e00714 --- /dev/null +++ b/app/contexts/ThemeContext.tsx @@ -0,0 +1,97 @@ +import {useContext, createContext, useState, useEffect} from "react" +import AsyncStorage from "@react-native-async-storage/async-storage" +import { useColorScheme } from "react-native"; +import themeColors from "../../constants/colors"; + + +interface ThemeProps { + isSystemTheme?: boolean + theme: "light" | "dark"; + colors: typeof themeColors.light & typeof themeColors.dark; + applySystemTheme?: () => void; + applyTheme?: (theme: "light" | "dark") => void; +} + +const THEME_KEY = "THEME" +const SYSTEM_THEME_KEY = "SYSTEM_THEME" + +const ThemeContext = createContext({theme: "light", colors: themeColors.light}) + +export const useTheme = () => { + return useContext(ThemeContext) +} + +export const ThemeProvider = ({children} : any) => { + const currentSystemTheme = useColorScheme() + const [isSystemTheme, setIsSystemTheme] = useState(true); + const [theme, setTheme] = useState<"light" | "dark">("light"); + const [colors, setColors] = useState(themeColors.light) + + useEffect(() => { + console.log("effect called") + const loadTheme = async () => { + try { + const is_system_theme = await AsyncStorage.getItem(SYSTEM_THEME_KEY) + const saved_theme = await AsyncStorage.getItem(THEME_KEY); + + if(is_system_theme !== "false"){//We will use !== false bacause system theme should be used if unspecified + applySystemTheme(); + return; + } + + if(saved_theme === "light" || saved_theme === "dark"){ + applyTheme(saved_theme); + } + + } catch (error) { + console.error(error) + } + } + loadTheme(); + }, [currentSystemTheme]) + + + const applySystemTheme = () => { + const storeSystemTheme = async () => { + console.log("applySystemTheme") + await AsyncStorage.setItem(SYSTEM_THEME_KEY, "true") + } + + storeSystemTheme(); + setIsSystemTheme(true) + applyThemeInternal(currentSystemTheme ?? "light") + } + + const applyThemeInternal = (theme: "light" | "dark") => { + const storeTheme = async (theme: "light" | "dark") => { + await AsyncStorage.setItem(THEME_KEY, theme) + } + + setTheme(theme); + storeTheme(theme); + setColors(themeColors[theme]) + } + + const applyTheme = (theme: "light" | "dark") => { + const unsetSytemTheme = async () => { + AsyncStorage.setItem(SYSTEM_THEME_KEY, "false"); + } + unsetSytemTheme() + setIsSystemTheme(false); + applyThemeInternal(theme); + } + + const value = { + isSystemTheme: isSystemTheme, + applyTheme: applyTheme, + applySystemTheme: applySystemTheme, + theme: theme, + colors: colors, + } + + return ( + + {children} + + ) +} diff --git a/package-lock.json b/package-lock.json index 2e67c4c..f07d162 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^13.0.0", + "@react-native-async-storage/async-storage": "1.18.2", "@react-navigation/native": "^6.0.2", "expo": "~49.0.15", "expo-font": "~11.4.0", @@ -4277,6 +4278,17 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.18.2.tgz", + "integrity": "sha512-dM8AfdoeIxlh+zqgr0o5+vCTPQ0Ru1mrPzONZMsr7ufp5h+6WgNxQNza7t0r5qQ6b04AJqTlBNixTWZxqP649Q==", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || 0.60 - 0.72 || 1000.0.0" + } + }, "node_modules/@react-native-community/cli": { "version": "11.3.7", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-11.3.7.tgz", @@ -13452,6 +13464,25 @@ "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge-options/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 8fff007..a625b05 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "@expo/vector-icons": "^13.0.0", + "@react-native-async-storage/async-storage": "1.18.2", "@react-navigation/native": "^6.0.2", "expo": "~49.0.15", "expo-font": "~11.4.0", From 798e54f11c885934827f5a7de6eb61ee4b0e39d4 Mon Sep 17 00:00:00 2001 From: Jakob Stornig Date: Sun, 17 Dec 2023 19:26:15 +0100 Subject: [PATCH 2/5] feat: UserSettings --- app/(tabs)/_layout.tsx | 3 +- app/(tabs)/home/_layout.tsx | 22 ++++- app/(tabs)/home/index.tsx | 4 +- app/(tabs)/home/userSettings.tsx | 109 +++++++++++++++++++++++ components/home/userSettings/Setting.tsx | 66 ++++++++++++++ components/index.tsx | 6 +- 6 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 app/(tabs)/home/userSettings.tsx create mode 100644 components/home/userSettings/Setting.tsx diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 299bba8..bbff5e2 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -53,7 +53,8 @@ export default function Layout() { tabBarLabel: "Home", tabBarIcon: ({size, color}) => ( ), - unmountOnBlur: true + unmountOnBlur: true, + href: "(tabs)/home/" } }/> + + ) } \ No newline at end of file diff --git a/app/(tabs)/home/index.tsx b/app/(tabs)/home/index.tsx index 8b406f8..132c2cf 100644 --- a/app/(tabs)/home/index.tsx +++ b/app/(tabs)/home/index.tsx @@ -7,6 +7,7 @@ import useFetch from '../../../hooks/useFetch'; import { useThemeColor } from "../../../hooks/useThemeColor"; import { addExpense } from "../../../services/database"; import { useAuth } from '../../contexts/AuthContext'; +import { useRouter } from "expo-router"; export default function Page() { @@ -31,6 +32,7 @@ export default function Page() { } }); + const router = useRouter(); const {onLogout} = useAuth(); const [plusShow, setPlusShow] = useState(true); const prevOffset = useRef(0); @@ -73,7 +75,7 @@ export default function Page() { data={data} ListHeaderComponent={ <> - + {router.push("/home/userSettings")}}> } diff --git a/app/(tabs)/home/userSettings.tsx b/app/(tabs)/home/userSettings.tsx new file mode 100644 index 0000000..755c305 --- /dev/null +++ b/app/(tabs)/home/userSettings.tsx @@ -0,0 +1,109 @@ +import { View, Text, StyleSheet, Image, Appearance } from 'react-native' +import React, { useState } from 'react' +import { useThemeColor } from '../../../hooks/useThemeColor' +import { SIZES } from '../../../constants/theme' +import { SafeAreaView } from 'react-native-safe-area-context' +import { ButtonSetting, ToggleSetting } from '../../../components' +import { useTheme } from '../../contexts/ThemeContext' +import { deleteExpenses } from '../../../services/database' +import { useAuth } from '../../contexts/AuthContext' +import { TouchableOpacity } from 'react-native-gesture-handler' + +const generateStyles = (): void => { + styles.text = { + color: useThemeColor('primaryText') + } +} + +export default function userSettings() { + const {onLogout} = useAuth(); + const {theme, colors, isSystemTheme, applyTheme, applySystemTheme} = useTheme(); + + const backgroundColor = useThemeColor("backgroundColor"); + styles.text = {...styles.text, color: useThemeColor("primaryText")} + + const [systemTheme, setSystemTheme] = useState(isSystemTheme!) + const [darkMode, setDarkMode] = useState(theme === "dark" ? true : false) + + const handleSystemTheme = () => { + if(systemTheme){ + applyTheme!(theme) + }else{ + applySystemTheme!() + } + + setSystemTheme(prev => !prev) + + } + + const handleDarkMode = () => { + const newTheme = darkMode ? "light" : "dark" + + setDarkMode(prev => !prev) + applyTheme!(newTheme); + } + + + return ( + + + + + My Profile + + + + + + { + deleteExpenses().then(() => { + console.log("Expenses Deleted!"); + })}} + /> + + + + + + + Sign Out + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "space-between" + }, + + header:{ + flexDirection: 'row', + alignItems: "center", + justifyContent: "space-between", + margin: 10, + }, + settingsContainer: { + marginHorizontal: 10 + }, + + upperPart:{ + flex: 1 + }, + + bottomNavigation:{ + marginBottom: 20, + alignItems:"center" + + }, + button:{ + borderRadius: 80, + minHeight: 60, + justifyContent: "center", + paddingHorizontal: 20 + }, + text: {} +}) \ No newline at end of file diff --git a/components/home/userSettings/Setting.tsx b/components/home/userSettings/Setting.tsx new file mode 100644 index 0000000..19b2123 --- /dev/null +++ b/components/home/userSettings/Setting.tsx @@ -0,0 +1,66 @@ +import { View, Text, StyleSheet, Switch, SwitchProps, useColorScheme, TouchableOpacityProps, TouchableOpacity, ViewProps } from 'react-native' +import React from 'react' +import { SIZES } from '../../../constants/theme' +import { useThemeColor } from '../../../hooks/useThemeColor'; +import { CustomCard } from "../../" +import { useTheme } from '../../../app/contexts/ThemeContext'; + +interface ToggleSettingProps extends SwitchProps { + settingsTitle: string; +} + +export function ToggleSetting(props: ToggleSettingProps) { + const {settingsTitle, ...rest} = props; + const { colors } = useTheme() + + return ( + + + + {settingsTitle} + + + + + ) +} + +interface ButtonSettingProps extends TouchableOpacityProps { + settingsTitle: string, +} + +export function ButtonSetting(props: ButtonSettingProps){ + const {settingsTitle, ...rest} = props; + const { colors } = useTheme(); + + return ( + + + {settingsTitle} + + + ) +} + + +const styles = StyleSheet.create({ + settingTitle: { + fontSize: SIZES.large + }, + settingsTile: { + width: "100%", + minHeight: 60, + paddingVertical: 5, + paddingHorizontal: 20, + borderRadius: 80, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + marginBottom: 10 + } +}) \ No newline at end of file diff --git a/components/index.tsx b/components/index.tsx index b95c4c8..7d697da 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -1,11 +1,13 @@ //home import ExpenseItem from "./home/expenseItem/expenseItem" import Welcome from "./home/Welcome/Welcome" +import { ToggleSetting, ButtonSetting } from "./home/userSettings/Setting" //common import LoadingSymbol from "./common/loadingSymbol/loadingSymbol" import Plus from "./common/plus/plus" import SearchBar from "./common/searchBar/SearchBar" +import CustomCard from "./common/customCard/CustomCard" //login @@ -14,6 +16,8 @@ import Input from "./login/input" export { ExpenseItem, Input, LoadingSymbol, Plus, - SearchBar, Welcome + SearchBar, Welcome, + ToggleSetting, CustomCard, + ButtonSetting } From 1a2ce85e9df7142cd01baeb014238773d170b974 Mon Sep 17 00:00:00 2001 From: Jakob Stornig Date: Sun, 17 Dec 2023 19:27:32 +0100 Subject: [PATCH 3/5] fix: similar colors rendered the sam way due to andriod color handling --- constants/colors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants/colors.ts b/constants/colors.ts index f197f71..6b8937d 100644 --- a/constants/colors.ts +++ b/constants/colors.ts @@ -22,7 +22,7 @@ export default { tabIconSelected: "#EF6C00", backgroundColor: "#121212", - containerColor: "#181818", + containerColor: "#1c1c1c", elementDefaultColor: "#535353", elementSelectedColor: "#B3B3B3", accentColor: "#EF6C00", From f445504f1086e919eca5af256ef4b6ac7db84222 Mon Sep 17 00:00:00 2001 From: Jakob Stornig Date: Mon, 18 Dec 2023 18:15:19 +0100 Subject: [PATCH 4/5] fix: Loaded Theme does not match selected theme when switching to manual theme --- app/(tabs)/home/userSettings.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/(tabs)/home/userSettings.tsx b/app/(tabs)/home/userSettings.tsx index 755c305..ec7a8d4 100644 --- a/app/(tabs)/home/userSettings.tsx +++ b/app/(tabs)/home/userSettings.tsx @@ -26,8 +26,9 @@ export default function userSettings() { const [darkMode, setDarkMode] = useState(theme === "dark" ? true : false) const handleSystemTheme = () => { + const newTheme = darkMode ? "dark" : "light" if(systemTheme){ - applyTheme!(theme) + applyTheme!(newTheme) }else{ applySystemTheme!() } From 86260d7a24d094dca1101a27de6702752bc5875b Mon Sep 17 00:00:00 2001 From: Jakob Stornig Date: Mon, 18 Dec 2023 18:16:16 +0100 Subject: [PATCH 5/5] fix: auth state not loaded properly, console cleanup --- app/(tabs)/_layout.tsx | 4 --- app/_layout.tsx | 1 - app/contexts/AuthContext.tsx | 58 +++++++++++++++--------------------- app/index.tsx | 3 +- 4 files changed, 25 insertions(+), 41 deletions(-) diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index bbff5e2..ab3ebb5 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -13,10 +13,6 @@ export default function Layout() { // const backgroundColor: string = useThemeColor("backgroundColor"); // const tabBarColor: string = useThemeColor("tabBarColor"); const {authState} = useAuth() - useEffect(()=>{ - console.log(authState, "in root Layout") - },[authState]) - const styles = StyleSheet.create({ sceneContainer: { diff --git a/app/_layout.tsx b/app/_layout.tsx index 0a38cb4..f34a4ea 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -10,7 +10,6 @@ export default function _layout() { initDatabase(); }, []); - console.log("layout called") return ( diff --git a/app/contexts/AuthContext.tsx b/app/contexts/AuthContext.tsx index 236c962..0cf1d8d 100644 --- a/app/contexts/AuthContext.tsx +++ b/app/contexts/AuthContext.tsx @@ -4,9 +4,10 @@ import * as SecureStore from 'expo-secure-store' interface AuthProps { authState?: {email: string | null; authenticated: boolean | null; session: string | null}; + isLoading?: boolean | null; onRegister?: (email: string, password: string) => Promise; - onLogin?: (email: string, password: string) => Promise - onLogout?: () => void + onLogin?: (email: string, password: string) => Promise; + onLogout?: () => void; } const SESSION_KEY = 'my_session' @@ -50,38 +51,31 @@ export const AuthProvider = ({children}: any) => { email: string | null; authenticated: boolean | null; session : string | null; - isLoading: boolean; }>({ - email: null, authenticated: null, session: null, isLoading: false + email: null, authenticated: null, session: null }); + const [isLoading, setIsLoading] = useState(true) useEffect(() => { const loadSession = async () => { - setAuthState((prev) => { - return { - ...prev, - isLoading: true - } - }) + setIsLoading(true) const session = await getStorageItemAsync(SESSION_KEY) const email = await getStorageItemAsync(USER_KEY) - if(session && email){ - setAuthState({ - email: email, - authenticated: true, - session: session, - isLoading: false + if(session !== null && email !== null){ + console.log("applied session", session, email) + setAuthState( (prev) => { + return { + email: email, + authenticated: true, + session: session, + } }) } - setAuthState((prev) => { - return { - ...prev, - isLoading: false - } - }) + + setIsLoading(false) } loadSession() },[]) @@ -92,29 +86,23 @@ export const AuthProvider = ({children}: any) => { } const login = async (email: string, password: string) => { - + setIsLoading(true) //login functionality goes here console.log("login called") - setAuthState((prev) => { - return{ - ...prev, - isLoading: true - } - }) setAuthState({ email: email, authenticated: true, - session: "super duper session token das wir vlt mal brauchen", - isLoading: false - + session: "super duper session token das wir vlt mal brauchen" }) await setStorageItemAsync(SESSION_KEY, "super duper session token das wir vlt mal brauchen") await setStorageItemAsync(USER_KEY, email) + setIsLoading(false) } const logout = async () => { + setIsLoading(true) await setStorageItemAsync(SESSION_KEY, null) await setStorageItemAsync(USER_KEY, null) @@ -122,8 +110,9 @@ export const AuthProvider = ({children}: any) => { email: null, authenticated: false, session: null, - isLoading: false }); + + setIsLoading(false) }; @@ -131,7 +120,8 @@ export const AuthProvider = ({children}: any) => { onRegister: register, onLogin: login, onLogout: logout, - authState + isLoading: isLoading, + authState: authState } return {children} } \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 094049b..265d68b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -6,9 +6,8 @@ import { useAuth } from './contexts/AuthContext' export default function index() { const {authState} = useAuth() - return ( - + ) } \ No newline at end of file