stabilize membership loading and home motivational message

This commit is contained in:
echo 2026-03-30 20:24:13 +02:00
parent a620921202
commit cd13333b52
5 changed files with 156 additions and 92 deletions

View File

@ -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;
}

View File

@ -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"}
</Text>
<Text style={[typography.body, { color: colors.textSecondary }]}>
{getMotivationalMessage()}
{motivationalMessage}
</Text>
</View>
<TouchableOpacity activeOpacity={0.8}>

View File

@ -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() {
<ThemeProvider>
<NotificationsProvider>
<StatisticsProvider>
<MembershipProvider>
<FitnessGoalsProvider>
<RecommendationsProvider>
<AppContent />
</RecommendationsProvider>
</FitnessGoalsProvider>
</MembershipProvider>
</StatisticsProvider>
</NotificationsProvider>
</ThemeProvider>

View File

@ -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<void>;
}
const MembershipContext = createContext<MembershipContextValue | undefined>(
undefined,
);
export function MembershipProvider({ children }: { children: ReactNode }) {
const { user } = useUser();
const { getToken, isSignedIn } = useAuth();
const [membershipType, setMembershipType] = useState<MembershipType>("basic");
const [features, setFeatures] = useState<MembershipFeatures>(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 (
<MembershipContext.Provider value={value}>
{children}
</MembershipContext.Provider>
);
}
export function useMembershipContext(): MembershipContextValue {
const context = useContext(MembershipContext);
if (!context) {
throw new Error(
"useMembershipContext must be used within MembershipProvider",
);
}
return context;
}

View File

@ -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<MembershipType>("basic");
const [features, setFeatures] = useState<MembershipFeatures>(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();
}