From 6b1f8f8b031e7686cb6645467e28dac3c3b4ea17 Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:36:13 +0200 Subject: [PATCH 1/8] :recycle: Refactor error handling to use Snackbar context for error messages --- ecoscan_app/app/(tabs)/(scan)/index.tsx | 35 ++++------------------ ecoscan_app/context/ErrorContext.tsx | 40 +++++++------------------ ecoscan_app/hooks/useAnalyzeProduct.ts | 6 +--- 3 files changed, 18 insertions(+), 63 deletions(-) diff --git a/ecoscan_app/app/(tabs)/(scan)/index.tsx b/ecoscan_app/app/(tabs)/(scan)/index.tsx index 6051d027..84b372b0 100644 --- a/ecoscan_app/app/(tabs)/(scan)/index.tsx +++ b/ecoscan_app/app/(tabs)/(scan)/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { KeyboardAvoidingView, Platform, @@ -7,7 +7,7 @@ import { TextInput, View, } from "react-native"; -import { ActivityIndicator, Button, Snackbar, Text } from "react-native-paper"; +import { ActivityIndicator, Button, Text } from "react-native-paper"; import BarcodeScanner from "@/components/BarcodeScanner"; import { PageContainer } from "@/components/PageContainer"; @@ -18,25 +18,18 @@ import { useError } from "@/context/ErrorContext"; export default function Scan() { const [barcode, setBarcode] = useState(""); - const [error, setError] = useState(); - const [snackbarVisible, setSnackbarVisible] = useState(false); + const { setError } = useError(); const router = useRouter(); const { loading, analyzeProduct, cancelAnalysis } = useAnalyzeProduct(); - const { consumeError } = useError(); const [scanned, setScanned] = useState(false); const scrollViewRef = useRef(null); - const showError = (message: string) => { - setError(message); - setSnackbarVisible(true); - }; - const onScanned = async (code: string) => { scrollViewRef.current?.scrollTo({ y: 0, animated: true }); setScanned(true); const trimmed = code.trim(); if (!trimmed) { - showError("Barcode darf nicht leer sein."); + setError("Barcode darf nicht leer sein."); return; } try { @@ -47,22 +40,15 @@ export default function Scan() { params: { id: trimmed }, }); } else { - showError("Produkt konnte nicht analysiert werden."); + setError("Produkt konnte nicht analysiert werden."); } } catch (err) { const msg = err instanceof Error ? err.message : "Analyse fehlgeschlagen."; - showError(msg); + setError(msg); } }; - useEffect(() => { - const errorMsg = consumeError(); - if (errorMsg) { - showError(errorMsg); - } - }, [consumeError]); - return ( )} - - setSnackbarVisible(false)} - duration={4000} - style={{ backgroundColor: theme.colors.error }} - > - {error} - diff --git a/ecoscan_app/context/ErrorContext.tsx b/ecoscan_app/context/ErrorContext.tsx index cf05e3a3..17bbef7d 100644 --- a/ecoscan_app/context/ErrorContext.tsx +++ b/ecoscan_app/context/ErrorContext.tsx @@ -4,37 +4,19 @@ import { useCallback, useContext, useRef, + useEffect, + useState, } from "react"; +import { useSnackbar } from "@/context/SnackbarContext"; +export function useError() { + const { showError } = useSnackbar(); -type ErrorContextType = { - setError: (msg: string) => void; - consumeError: () => string | undefined; -}; - -const ErrorContext = createContext(undefined); - -export function ErrorProvider({ children }: { children: ReactNode }) { - const errorRef = useRef(undefined); - - const setError = useCallback((msg: string) => { - errorRef.current = msg; - }, []); - - const consumeError = useCallback(() => { - const msg = errorRef.current; - errorRef.current = undefined; - return msg; - }, []); - - return ( - - {children} - + const setError = useCallback( + (msg: string, duration?: number) => { + showError(msg, duration ?? 5000); + }, + [showError], ); -} -export function useError() { - const ctx = useContext(ErrorContext); - if (!ctx) throw new Error("useError must be used within ErrorProvider"); - return ctx; + return { setError }; } diff --git a/ecoscan_app/hooks/useAnalyzeProduct.ts b/ecoscan_app/hooks/useAnalyzeProduct.ts index 52558505..7953f654 100644 --- a/ecoscan_app/hooks/useAnalyzeProduct.ts +++ b/ecoscan_app/hooks/useAnalyzeProduct.ts @@ -89,11 +89,7 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { }); } catch (err) { setLoading(false); - const errorMsg = - err instanceof Error - ? err.message - : "Produkt konnte nicht analysiert werden."; - setError(errorMsg); + console.warn("[useAnalyzeProduct] " + err); throw err; } }, From dfc5d7b2a2ae93b4b82f276d43c0c6e3ebf87515 Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:39:49 +0200 Subject: [PATCH 2/8] :sparkles: Implement Snackbar context for global notifications in layout --- ecoscan_app/app/_layout.tsx | 38 +++++-- ecoscan_app/context/SnackbarContext.tsx | 144 ++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 ecoscan_app/context/SnackbarContext.tsx diff --git a/ecoscan_app/app/_layout.tsx b/ecoscan_app/app/_layout.tsx index 0099e7dc..7e7c44db 100644 --- a/ecoscan_app/app/_layout.tsx +++ b/ecoscan_app/app/_layout.tsx @@ -4,11 +4,15 @@ import { useRouter, useSegments, } from "expo-router"; -import { PaperProvider } from "react-native-paper"; +import { PaperProvider, Snackbar } from "react-native-paper"; import { theme } from "@/theme"; import { AuthProvider, useAuth } from "@/context/AuthContext"; import { useEffect } from "react"; -import { ErrorProvider } from "@/context/ErrorContext"; +import { + useSnackbar, + getSnackbarStyles, + SnackbarProvider, +} from "@/context/SnackbarContext"; import { NotificationProvider } from "@/context/NotificationProvider"; import { ProductProvider } from "@/context/ProductContext"; @@ -17,6 +21,7 @@ function RootLayoutNav() { const segments = useSegments(); const router = useRouter(); const navigationState = useRootNavigationState(); + const { currentSnackbar, dismissSnackbar } = useSnackbar(); useEffect(() => { if (!navigationState?.key || isLoading) return; @@ -44,23 +49,40 @@ function RootLayoutNav() { ); - return isAuthenticated && !isLoading ? ( - {stack} - ) : ( - stack + return ( + <> + {isAuthenticated && !isLoading ? ( + {stack} + ) : ( + stack + )} + {currentSnackbar && ( + + {currentSnackbar.message} + + )} + ); } export default function RootLayout() { return ( - + - + ); } diff --git a/ecoscan_app/context/SnackbarContext.tsx b/ecoscan_app/context/SnackbarContext.tsx new file mode 100644 index 00000000..5ac9b4c9 --- /dev/null +++ b/ecoscan_app/context/SnackbarContext.tsx @@ -0,0 +1,144 @@ +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; + +import { theme } from "@/theme"; + +export type SnackbarType = "error" | "success" | "info" | "warning"; + +type Snackbar = { + id: string; + message: string; + type: SnackbarType; + duration?: number; +}; + +type SnackbarContextType = { + showError: (msg: string, duration?: number) => void; + showSuccess: (msg: string, duration?: number) => void; + showInfo: (msg: string, duration?: number) => void; + showWarning: (msg: string, duration?: number) => void; + currentSnackbar: Snackbar | undefined; + dismissSnackbar: () => void; +}; + +const SnackbarContext = createContext( + undefined, +); + +export const getSnackbarStyles = (type: SnackbarType) => { + switch (type) { + case "error": + return { + backgroundColor: theme.colors.error, + }; + case "success": + return { + backgroundColor: theme.colors.success, + }; + case "warning": + return { + backgroundColor: theme.colors.warning, + }; + case "info": + return { + backgroundColor: theme.colors.info, + }; + default: + return { + backgroundColor: theme.colors.surface, + }; + } +}; + +export function SnackbarProvider({ children }: { children: ReactNode }) { + const [snackbarQueue, setSnackbarQueue] = useState([]); + const [currentSnackbar, setCurrentSnackbar] = useState< + Snackbar | undefined + >(); + + const addSnackbar = useCallback( + (message: string, type: SnackbarType, duration?: number) => { + const id = Date.now().toString(); + setSnackbarQueue((prev) => [ + ...prev, + { id, message, type, duration: duration ?? 4000 }, + ]); + }, + [], + ); + + const showError = useCallback( + (msg: string, duration?: number) => { + addSnackbar(msg, "error", duration ?? 5000); + }, + [addSnackbar], + ); + + const showSuccess = useCallback( + (msg: string, duration?: number) => { + addSnackbar(msg, "success", duration ?? 3000); + }, + [addSnackbar], + ); + + const showInfo = useCallback( + (msg: string, duration?: number) => { + addSnackbar(msg, "info", duration ?? 3000); + }, + [addSnackbar], + ); + + const showWarning = useCallback( + (msg: string, duration?: number) => { + addSnackbar(msg, "warning", duration ?? 4000); + }, + [addSnackbar], + ); + + const dismissSnackbar = useCallback(() => { + setCurrentSnackbar(undefined); + }, []); + + useEffect(() => { + if (!currentSnackbar && snackbarQueue.length > 0) { + const [next, ...rest] = snackbarQueue; + setCurrentSnackbar(next); + setSnackbarQueue(rest); + } + }, [currentSnackbar, snackbarQueue]); + + useEffect(() => { + if (!currentSnackbar) return; + + const timer = setTimeout(dismissSnackbar, currentSnackbar.duration); + + return () => clearTimeout(timer); + }, [currentSnackbar, dismissSnackbar]); + + return ( + + {children} + + ); +} + +export function useSnackbar() { + const ctx = useContext(SnackbarContext); + if (!ctx) throw new Error("useSnackbar must be used within SnackbarProvider"); + return ctx; +} From 03ce681c539c11557c14c3d05e7b1ef7b983449c Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:39:23 +0200 Subject: [PATCH 3/8] :art: Update SnackbarContext colors for improved visual consistency --- ecoscan_app/context/SnackbarContext.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ecoscan_app/context/SnackbarContext.tsx b/ecoscan_app/context/SnackbarContext.tsx index 5ac9b4c9..36060295 100644 --- a/ecoscan_app/context/SnackbarContext.tsx +++ b/ecoscan_app/context/SnackbarContext.tsx @@ -39,7 +39,7 @@ export const getSnackbarStyles = (type: SnackbarType) => { }; case "success": return { - backgroundColor: theme.colors.success, + backgroundColor: theme.colors.primary, }; case "warning": return { @@ -47,7 +47,7 @@ export const getSnackbarStyles = (type: SnackbarType) => { }; case "info": return { - backgroundColor: theme.colors.info, + backgroundColor: theme.colors.surface, }; default: return { From ee357f6cdd950c389298469b4e3db51225fcaf4c Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:49:29 +0200 Subject: [PATCH 4/8] :recycle: Optimize SnackbarContext provider value with useMemo for performance --- ecoscan_app/context/SnackbarContext.tsx | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/ecoscan_app/context/SnackbarContext.tsx b/ecoscan_app/context/SnackbarContext.tsx index 36060295..afd89718 100644 --- a/ecoscan_app/context/SnackbarContext.tsx +++ b/ecoscan_app/context/SnackbarContext.tsx @@ -5,6 +5,7 @@ import { useContext, useEffect, useState, + useMemo, } from "react"; import { theme } from "@/theme"; @@ -121,17 +122,27 @@ export function SnackbarProvider({ children }: { children: ReactNode }) { return () => clearTimeout(timer); }, [currentSnackbar, dismissSnackbar]); + const contextValue = useMemo( + () => ({ + showError, + showSuccess, + showInfo, + showWarning, + currentSnackbar, + dismissSnackbar, + }), + [ + showError, + showSuccess, + showInfo, + showWarning, + currentSnackbar, + dismissSnackbar, + ], + ); + return ( - + {children} ); From 28f72e7fd880e90f8bfa6f77a4ef98e0d53a02ea Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 11:56:30 +0200 Subject: [PATCH 5/8] :recycle: Integrate Snackbar context for success and error notifications in BoughtButton --- .../components/product/BoughtButton.tsx | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/ecoscan_app/components/product/BoughtButton.tsx b/ecoscan_app/components/product/BoughtButton.tsx index d929a397..680a4291 100644 --- a/ecoscan_app/components/product/BoughtButton.tsx +++ b/ecoscan_app/components/product/BoughtButton.tsx @@ -1,9 +1,9 @@ -import { Button, Snackbar, Portal } from "react-native-paper"; +import { Button } from "react-native-paper"; import { StyleSheet } from "react-native"; import { Product } from "@/types/product"; import { useSaveProduct } from "@/hooks/useSaveProduct"; -import { useState, useEffect } from "react"; -import { theme } from "@/theme"; +import { useEffect } from "react"; +import { useSnackbar } from "@/context/SnackbarContext"; export interface BoughtButtonProps { product?: Product; @@ -12,13 +12,15 @@ export interface BoughtButtonProps { export default function BoughtButton({ product }: BoughtButtonProps) { const { saveProduct, loading, error, success, saved, resetSaved } = useSaveProduct(); - const [visible, setVisible] = useState(false); + const { showSuccess, showError } = useSnackbar(); useEffect(() => { - if (success || error) { - setVisible(true); + if (success) { + showSuccess("Produkt erfolgreich gekauft!"); + } else if (error) { + showError("Fehler beim Speichern des Produkts."); } - }, [success, error]); + }, [success, error, showSuccess, showError]); useEffect(() => { resetSaved(); @@ -43,19 +45,6 @@ export default function BoughtButton({ product }: BoughtButtonProps) { > Gekauft - - - setVisible(false)} - duration={2000} - style={{ - backgroundColor: error ? theme.colors.error : theme.colors.primary, - }} - > - {error ? `${error}` : "Erfolgreich gespeichert"} - - ); } From b00d109c2081fa648ecabf49e7a78f3695de302b Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:56:47 +0200 Subject: [PATCH 6/8] :goal_net: Update error handling and log errors to console --- ecoscan_app/hooks/useAnalyzeProduct.ts | 37 +++++++++----------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/ecoscan_app/hooks/useAnalyzeProduct.ts b/ecoscan_app/hooks/useAnalyzeProduct.ts index 7953f654..f5389a78 100644 --- a/ecoscan_app/hooks/useAnalyzeProduct.ts +++ b/ecoscan_app/hooks/useAnalyzeProduct.ts @@ -14,7 +14,6 @@ type UseAnalyzeProductResult = { export function useAnalyzeProduct(): UseAnalyzeProductResult { const api = useApiClient(); const { setProduct } = useProduct(); - const { setError } = useError(); const [loading, setLoading] = useState(false); const { startStream, closeStream } = useSseClient( "product-analysis-evaluation", @@ -25,21 +24,17 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { reject: (err?: any) => void; } | null>(null); - const handleStreamError = useCallback( - (err?: any) => { - closeStream(); - setLoading(false); - const errorMsg = - err instanceof Error && err.name === "AbortError" - ? "Analyse abgebrochen" - : "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."; - setError(errorMsg); - console.error("SSE stream error:", err); - completionRef.current?.reject(new Error(errorMsg)); - completionRef.current = null; - }, - [setError], - ); + const handleStreamError = useCallback((err?: any) => { + closeStream(); + setLoading(false); + const errorMsg = + err instanceof Error && err.name === "AbortError" + ? "Analyse abgebrochen" + : "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."; + console.warn("[useAnalyzeProduct] SSE stream error:", err); + completionRef.current?.reject(new Error(errorMsg)); + completionRef.current = null; + }, []); const handleStreamSuccess = useCallback( (result: Product) => { @@ -76,7 +71,6 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { setProduct(productData); return true; } - setError("Produkt konnte nicht geladen werden."); return false; } return new Promise((resolve, reject) => { @@ -93,14 +87,7 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { throw err; } }, - [ - api, - setProduct, - setError, - startStream, - handleStreamSuccess, - handleStreamError, - ], + [api, setProduct, startStream, handleStreamSuccess, handleStreamError], ); useEffect(() => { From 79bcf3fd517594723d32e77db3c0d73f74f16153 Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:57:00 +0200 Subject: [PATCH 7/8] :recycle: Refactor Snackbar component for dynamic duration --- ecoscan_app/app/_layout.tsx | 24 +++++++++++------------- ecoscan_app/context/SnackbarContext.tsx | 8 -------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/ecoscan_app/app/_layout.tsx b/ecoscan_app/app/_layout.tsx index 7e7c44db..b460ae51 100644 --- a/ecoscan_app/app/_layout.tsx +++ b/ecoscan_app/app/_layout.tsx @@ -56,19 +56,17 @@ function RootLayoutNav() { ) : ( stack )} - {currentSnackbar && ( - - {currentSnackbar.message} - - )} + + {currentSnackbar?.message || ""} + ); } diff --git a/ecoscan_app/context/SnackbarContext.tsx b/ecoscan_app/context/SnackbarContext.tsx index afd89718..835242c7 100644 --- a/ecoscan_app/context/SnackbarContext.tsx +++ b/ecoscan_app/context/SnackbarContext.tsx @@ -114,14 +114,6 @@ export function SnackbarProvider({ children }: { children: ReactNode }) { } }, [currentSnackbar, snackbarQueue]); - useEffect(() => { - if (!currentSnackbar) return; - - const timer = setTimeout(dismissSnackbar, currentSnackbar.duration); - - return () => clearTimeout(timer); - }, [currentSnackbar, dismissSnackbar]); - const contextValue = useMemo( () => ({ showError, From 0cc05e0d711819a06fff684c16ddac7613eab17c Mon Sep 17 00:00:00 2001 From: IamPekka058 <59747867+IamPekka058@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:03:28 +0200 Subject: [PATCH 8/8] :recycle: Update dependencies in useAnalyzeProduct for improved error handling --- ecoscan_app/hooks/useAnalyzeProduct.ts | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/ecoscan_app/hooks/useAnalyzeProduct.ts b/ecoscan_app/hooks/useAnalyzeProduct.ts index f5389a78..17f7ebb5 100644 --- a/ecoscan_app/hooks/useAnalyzeProduct.ts +++ b/ecoscan_app/hooks/useAnalyzeProduct.ts @@ -24,17 +24,20 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { reject: (err?: any) => void; } | null>(null); - const handleStreamError = useCallback((err?: any) => { - closeStream(); - setLoading(false); - const errorMsg = - err instanceof Error && err.name === "AbortError" - ? "Analyse abgebrochen" - : "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."; - console.warn("[useAnalyzeProduct] SSE stream error:", err); - completionRef.current?.reject(new Error(errorMsg)); - completionRef.current = null; - }, []); + const handleStreamError = useCallback( + (err?: any) => { + closeStream(); + setLoading(false); + const errorMsg = + err instanceof Error && err.name === "AbortError" + ? "Analyse abgebrochen" + : "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut."; + console.warn("[useAnalyzeProduct] SSE stream error:", err); + completionRef.current?.reject(new Error(errorMsg)); + completionRef.current = null; + }, + [closeStream], + ); const handleStreamSuccess = useCallback( (result: Product) => { @@ -44,7 +47,7 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { completionRef.current?.resolve(true); completionRef.current = null; }, - [setProduct], + [setProduct, closeStream], ); const cancelAnalysis = useCallback(() => { @@ -83,7 +86,7 @@ export function useAnalyzeProduct(): UseAnalyzeProductResult { }); } catch (err) { setLoading(false); - console.warn("[useAnalyzeProduct] " + err); + console.warn("[useAnalyzeProduct] analyzeProduct failed:", err); throw err; } },