From cd13333b52ce3f8003f9d2ef2ed44a0f26b9768c Mon Sep 17 00:00:00 2001 From: echo Date: Mon, 30 Mar 2026 20:24:13 +0200 Subject: [PATCH] stabilize membership loading and home motivational message --- apps/admin/src/middleware.ts | 1 - apps/mobile/src/app/(tabs)/index.tsx | 61 +++++++++--- apps/mobile/src/app/_layout.tsx | 13 ++- .../mobile/src/contexts/MembershipContext.tsx | 96 +++++++++++++++++++ apps/mobile/src/hooks/useMembership.ts | 77 +-------------- 5 files changed, 156 insertions(+), 92 deletions(-) create mode 100644 apps/mobile/src/contexts/MembershipContext.tsx diff --git a/apps/admin/src/middleware.ts b/apps/admin/src/middleware.ts index ef32070..0081674 100644 --- a/apps/admin/src/middleware.ts +++ b/apps/admin/src/middleware.ts @@ -34,7 +34,6 @@ export default clerkMiddleware(async (auth, req) => { // For API routes, let the route handler check auth // This allows API routes to handle both web sessions and mobile Bearer tokens if (isApiRoute(req)) { - log.debug("API route, auth will be checked in handler"); return; } diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 0933f02..9ae79de 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -42,6 +42,18 @@ import { const CALORIE_GOAL = 2000; const WATER_GOAL = 2000; const WORKOUT_GOAL = 3; +const MOTIVATION_KEY_PREFIX = "home-motivation"; + +const getRandomMotivation = () => { + const messages = [ + "Let's crush it today! 💪", + "Ready to level up? 🔥", + "You've got this! ⚡", + "Time to shine! ✨", + "Let's make it happen! 🚀", + ]; + return messages[Math.floor(Math.random() * messages.length)]; +}; export default function HomeScreen() { const { user } = useUser(); @@ -56,6 +68,9 @@ export default function HomeScreen() { const [scanFoodModalVisible, setScanFoodModalVisible] = useState(false); const [calories, setCalories] = useState(0); const [waterIntake, setWaterIntake] = useState(0); + const [motivationalMessage, setMotivationalMessage] = useState( + "Let's crush it today! 💪", + ); const caloriesBounce = useRef(new Animated.Value(1)).current; const waterBounce = useRef(new Animated.Value(1)).current; @@ -80,17 +95,6 @@ export default function HomeScreen() { return "Good Evening"; }; - const getMotivationalMessage = () => { - const messages = [ - "Let's crush it today! 💪", - "Ready to level up? 🔥", - "You've got this! ⚡", - "Time to shine! ✨", - "Let's make it happen! 🚀", - ]; - return messages[Math.floor(Math.random() * messages.length)]; - }; - const handleSaveMeal = (meal: { type: string; name: string; @@ -145,6 +149,39 @@ export default function HomeScreen() { await AsyncStorage.removeItem(`water_${today}`); }; + useEffect(() => { + const loadDailyMotivation = async () => { + const today = new Date().toISOString().split("T")[0]; + const storageKey = `${MOTIVATION_KEY_PREFIX}_${user?.id || "guest"}`; + const storedValue = await AsyncStorage.getItem(storageKey); + + if (storedValue) { + try { + const parsed = JSON.parse(storedValue) as { + date: string; + message: string; + }; + + if (parsed.date === today && parsed.message) { + setMotivationalMessage(parsed.message); + return; + } + } catch { + // Ignore invalid local value and regenerate + } + } + + const nextMessage = getRandomMotivation(); + setMotivationalMessage(nextMessage); + await AsyncStorage.setItem( + storageKey, + JSON.stringify({ date: today, message: nextMessage }), + ); + }; + + loadDailyMotivation(); + }, [user?.id]); + useEffect(() => { const loadPersistedData = async () => { const today = new Date().toDateString(); @@ -238,7 +275,7 @@ export default function HomeScreen() { {user?.firstName || "Champion"} - {getMotivationalMessage()} + {motivationalMessage} diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 598903a..2f5afc0 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -11,6 +11,7 @@ import { StatisticsProvider } from "../contexts/StatisticsContext"; import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext"; import { RecommendationsProvider } from "../contexts/RecommendationsContext"; import { NotificationsProvider } from "../contexts/NotificationsContext"; +import { MembershipProvider } from "../contexts/MembershipContext"; import { queryClient } from "../lib/query-client"; import log from "../utils/logger"; @@ -180,11 +181,13 @@ export default function RootLayout() { - - - - - + + + + + + + diff --git a/apps/mobile/src/contexts/MembershipContext.tsx b/apps/mobile/src/contexts/MembershipContext.tsx new file mode 100644 index 0000000..094c4c8 --- /dev/null +++ b/apps/mobile/src/contexts/MembershipContext.tsx @@ -0,0 +1,96 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import { useAuth, useUser } from "@clerk/clerk-expo"; +import { + getCurrentMembershipFeaturesFromServer, + type MembershipFeatures, + type MembershipType, +} from "../api/membership"; +import log from "../utils/logger"; + +const BASIC_FEATURES: MembershipFeatures = { + recommendationsPerMonth: 1, + hydrationTracking: false, + nutritionTracking: false, + advancedStatistics: false, +}; + +interface MembershipContextValue { + membershipType: MembershipType; + features: MembershipFeatures; + loading: boolean; + refreshMembership: () => Promise; +} + +const MembershipContext = createContext( + undefined, +); + +export function MembershipProvider({ children }: { children: ReactNode }) { + const { user } = useUser(); + const { getToken, isSignedIn } = useAuth(); + const [membershipType, setMembershipType] = useState("basic"); + const [features, setFeatures] = useState(BASIC_FEATURES); + const [loading, setLoading] = useState(true); + + const loadMembership = useCallback(async () => { + if (!isSignedIn || !user?.id) { + setMembershipType("basic"); + setFeatures(BASIC_FEATURES); + setLoading(false); + return; + } + + try { + setLoading(true); + const token = await getToken(); + const result = await getCurrentMembershipFeaturesFromServer(token); + setMembershipType(result.membershipType); + setFeatures(result.features); + } catch (error) { + log.error("Failed to load membership", error, { userId: user.id }); + setMembershipType("basic"); + setFeatures(BASIC_FEATURES); + } finally { + setLoading(false); + } + }, [isSignedIn, user?.id]); + + useEffect(() => { + loadMembership(); + }, [loadMembership]); + + const value = useMemo( + () => ({ + membershipType, + features, + loading, + refreshMembership: loadMembership, + }), + [membershipType, features, loading, loadMembership], + ); + + return ( + + {children} + + ); +} + +export function useMembershipContext(): MembershipContextValue { + const context = useContext(MembershipContext); + if (!context) { + throw new Error( + "useMembershipContext must be used within MembershipProvider", + ); + } + + return context; +} diff --git a/apps/mobile/src/hooks/useMembership.ts b/apps/mobile/src/hooks/useMembership.ts index 47ff848..2d97c31 100644 --- a/apps/mobile/src/hooks/useMembership.ts +++ b/apps/mobile/src/hooks/useMembership.ts @@ -1,76 +1,5 @@ -import { useAuth, useUser } from "@clerk/clerk-expo"; -import { useEffect, useState } from "react"; -import { - getCurrentMembershipFeaturesFromServer, - type MembershipFeatures, - type MembershipType, -} from "../api/membership"; -import log from "../utils/logger"; +import { useMembershipContext } from "../contexts/MembershipContext"; -const BASIC_FEATURES: MembershipFeatures = { - recommendationsPerMonth: 1, - hydrationTracking: false, - nutritionTracking: false, - advancedStatistics: false, -}; - -interface UseMembershipResult { - membershipType: MembershipType; - features: MembershipFeatures; - loading: boolean; -} - -export function useMembership(): UseMembershipResult { - const { user } = useUser(); - const { getToken, isSignedIn } = useAuth(); - const [membershipType, setMembershipType] = useState("basic"); - const [features, setFeatures] = useState(BASIC_FEATURES); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let isMounted = true; - - const loadMembership = async () => { - if (!isSignedIn || !user?.id) { - if (isMounted) { - setMembershipType("basic"); - setFeatures(BASIC_FEATURES); - setLoading(false); - } - return; - } - - try { - setLoading(true); - const token = await getToken(); - const result = await getCurrentMembershipFeaturesFromServer(token); - if (isMounted) { - setMembershipType(result.membershipType); - setFeatures(result.features); - } - } catch (error) { - log.error("Failed to load membership", error, { userId: user.id }); - if (isMounted) { - setMembershipType("basic"); - setFeatures(BASIC_FEATURES); - } - } finally { - if (isMounted) { - setLoading(false); - } - } - }; - - loadMembership(); - - return () => { - isMounted = false; - }; - }, [isSignedIn, user?.id, getToken]); - - return { - membershipType, - features, - loading, - }; +export function useMembership() { + return useMembershipContext(); }