stabilize membership loading and home motivational message
This commit is contained in:
parent
a620921202
commit
cd13333b52
@ -34,7 +34,6 @@ export default clerkMiddleware(async (auth, req) => {
|
|||||||
// For API routes, let the route handler check auth
|
// For API routes, let the route handler check auth
|
||||||
// This allows API routes to handle both web sessions and mobile Bearer tokens
|
// This allows API routes to handle both web sessions and mobile Bearer tokens
|
||||||
if (isApiRoute(req)) {
|
if (isApiRoute(req)) {
|
||||||
log.debug("API route, auth will be checked in handler");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,18 @@ import {
|
|||||||
const CALORIE_GOAL = 2000;
|
const CALORIE_GOAL = 2000;
|
||||||
const WATER_GOAL = 2000;
|
const WATER_GOAL = 2000;
|
||||||
const WORKOUT_GOAL = 3;
|
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() {
|
export default function HomeScreen() {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
@ -56,6 +68,9 @@ export default function HomeScreen() {
|
|||||||
const [scanFoodModalVisible, setScanFoodModalVisible] = useState(false);
|
const [scanFoodModalVisible, setScanFoodModalVisible] = useState(false);
|
||||||
const [calories, setCalories] = useState(0);
|
const [calories, setCalories] = useState(0);
|
||||||
const [waterIntake, setWaterIntake] = 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 caloriesBounce = useRef(new Animated.Value(1)).current;
|
||||||
const waterBounce = 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";
|
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: {
|
const handleSaveMeal = (meal: {
|
||||||
type: string;
|
type: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -145,6 +149,39 @@ export default function HomeScreen() {
|
|||||||
await AsyncStorage.removeItem(`water_${today}`);
|
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(() => {
|
useEffect(() => {
|
||||||
const loadPersistedData = async () => {
|
const loadPersistedData = async () => {
|
||||||
const today = new Date().toDateString();
|
const today = new Date().toDateString();
|
||||||
@ -238,7 +275,7 @@ export default function HomeScreen() {
|
|||||||
{user?.firstName || "Champion"}
|
{user?.firstName || "Champion"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[typography.body, { color: colors.textSecondary }]}>
|
<Text style={[typography.body, { color: colors.textSecondary }]}>
|
||||||
{getMotivationalMessage()}
|
{motivationalMessage}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity activeOpacity={0.8}>
|
<TouchableOpacity activeOpacity={0.8}>
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { StatisticsProvider } from "../contexts/StatisticsContext";
|
|||||||
import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext";
|
import { FitnessGoalsProvider } from "../contexts/FitnessGoalsContext";
|
||||||
import { RecommendationsProvider } from "../contexts/RecommendationsContext";
|
import { RecommendationsProvider } from "../contexts/RecommendationsContext";
|
||||||
import { NotificationsProvider } from "../contexts/NotificationsContext";
|
import { NotificationsProvider } from "../contexts/NotificationsContext";
|
||||||
|
import { MembershipProvider } from "../contexts/MembershipContext";
|
||||||
import { queryClient } from "../lib/query-client";
|
import { queryClient } from "../lib/query-client";
|
||||||
import log from "../utils/logger";
|
import log from "../utils/logger";
|
||||||
|
|
||||||
@ -180,11 +181,13 @@ export default function RootLayout() {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<NotificationsProvider>
|
<NotificationsProvider>
|
||||||
<StatisticsProvider>
|
<StatisticsProvider>
|
||||||
<FitnessGoalsProvider>
|
<MembershipProvider>
|
||||||
<RecommendationsProvider>
|
<FitnessGoalsProvider>
|
||||||
<AppContent />
|
<RecommendationsProvider>
|
||||||
</RecommendationsProvider>
|
<AppContent />
|
||||||
</FitnessGoalsProvider>
|
</RecommendationsProvider>
|
||||||
|
</FitnessGoalsProvider>
|
||||||
|
</MembershipProvider>
|
||||||
</StatisticsProvider>
|
</StatisticsProvider>
|
||||||
</NotificationsProvider>
|
</NotificationsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
96
apps/mobile/src/contexts/MembershipContext.tsx
Normal file
96
apps/mobile/src/contexts/MembershipContext.tsx
Normal 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;
|
||||||
|
}
|
||||||
@ -1,76 +1,5 @@
|
|||||||
import { useAuth, useUser } from "@clerk/clerk-expo";
|
import { useMembershipContext } from "../contexts/MembershipContext";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import {
|
|
||||||
getCurrentMembershipFeaturesFromServer,
|
|
||||||
type MembershipFeatures,
|
|
||||||
type MembershipType,
|
|
||||||
} from "../api/membership";
|
|
||||||
import log from "../utils/logger";
|
|
||||||
|
|
||||||
const BASIC_FEATURES: MembershipFeatures = {
|
export function useMembership() {
|
||||||
recommendationsPerMonth: 1,
|
return useMembershipContext();
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user