Compare commits

..

2 Commits

Author SHA1 Message Date
7ada05da6a db up 2026-03-29 19:54:17 +02:00
50ece15089 add membership features endpoint and use it in mobile 2026-03-29 19:51:36 +02:00
5 changed files with 90 additions and 6 deletions

Binary file not shown.

View File

@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { MEMBERSHIP_FEATURES } from "@/lib/membership/features";
import { getUserMembershipContext } from "@/lib/membership/access";
export async function GET() {
try {
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { membershipType, features } = await getUserMembershipContext(userId);
return NextResponse.json({
success: true,
data: {
membershipType,
currentFeatures: features,
plans: MEMBERSHIP_FEATURES,
},
meta: {
timestamp: new Date().toISOString(),
},
});
} catch {
return NextResponse.json(
{ error: "Failed to load membership features" },
{ status: 500 },
);
}
}

View File

@ -51,6 +51,15 @@ interface UsersListResponse {
}>; }>;
} }
interface MembershipFeaturesResponse {
success: boolean;
data: {
membershipType: MembershipType;
currentFeatures: MembershipFeatures;
plans: Record<MembershipType, MembershipFeatures>;
};
}
function isMembershipType(value: unknown): value is MembershipType { function isMembershipType(value: unknown): value is MembershipType {
return value === "basic" || value === "premium" || value === "vip"; return value === "basic" || value === "premium" || value === "vip";
} }
@ -85,3 +94,35 @@ export function getMembershipFeatures(
): MembershipFeatures { ): MembershipFeatures {
return MEMBERSHIP_FEATURES[membershipType]; return MEMBERSHIP_FEATURES[membershipType];
} }
export async function getCurrentMembershipFeaturesFromServer(
token: string | null,
): Promise<{
membershipType: MembershipType;
features: MembershipFeatures;
}> {
if (!token) {
return {
membershipType: "basic",
features: MEMBERSHIP_FEATURES.basic,
};
}
const response = await apiClient.get<MembershipFeaturesResponse>(
API_ENDPOINTS.MEMBERSHIP.FEATURES,
withAuth(token),
);
const data = response.data?.data;
if (!data) {
return {
membershipType: "basic",
features: MEMBERSHIP_FEATURES.basic,
};
}
return {
membershipType: data.membershipType,
features: data.currentFeatures,
};
}

View File

@ -44,6 +44,9 @@ export const API_ENDPOINTS = {
HISTORY: "/api/attendance/history", HISTORY: "/api/attendance/history",
}, },
RECOMMENDATIONS: "/api/recommendations", RECOMMENDATIONS: "/api/recommendations",
MEMBERSHIP: {
FEATURES: "/api/membership/features",
},
NUTRITION: { NUTRITION: {
BASE: "/api/nutrition", BASE: "/api/nutrition",
MEALS: "/api/nutrition/meals", MEALS: "/api/nutrition/meals",

View File

@ -1,14 +1,18 @@
import { useAuth, useUser } from "@clerk/clerk-expo"; import { useAuth, useUser } from "@clerk/clerk-expo";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
getCurrentMembershipType, getCurrentMembershipFeaturesFromServer,
getMembershipFeatures,
type MembershipFeatures, type MembershipFeatures,
type MembershipType, type MembershipType,
} from "../api/membership"; } from "../api/membership";
import log from "../utils/logger"; import log from "../utils/logger";
const BASIC_FEATURES = getMembershipFeatures("basic"); const BASIC_FEATURES: MembershipFeatures = {
recommendationsPerMonth: 1,
hydrationTracking: false,
nutritionTracking: false,
advancedStatistics: false,
};
interface UseMembershipResult { interface UseMembershipResult {
membershipType: MembershipType; membershipType: MembershipType;
@ -20,6 +24,7 @@ export function useMembership(): UseMembershipResult {
const { user } = useUser(); const { user } = useUser();
const { getToken, isSignedIn } = useAuth(); const { getToken, isSignedIn } = useAuth();
const [membershipType, setMembershipType] = useState<MembershipType>("basic"); const [membershipType, setMembershipType] = useState<MembershipType>("basic");
const [features, setFeatures] = useState<MembershipFeatures>(BASIC_FEATURES);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -29,6 +34,7 @@ export function useMembership(): UseMembershipResult {
if (!isSignedIn || !user?.id) { if (!isSignedIn || !user?.id) {
if (isMounted) { if (isMounted) {
setMembershipType("basic"); setMembershipType("basic");
setFeatures(BASIC_FEATURES);
setLoading(false); setLoading(false);
} }
return; return;
@ -37,14 +43,16 @@ export function useMembership(): UseMembershipResult {
try { try {
setLoading(true); setLoading(true);
const token = await getToken(); const token = await getToken();
const type = await getCurrentMembershipType(user.id, token); const result = await getCurrentMembershipFeaturesFromServer(token);
if (isMounted) { if (isMounted) {
setMembershipType(type); setMembershipType(result.membershipType);
setFeatures(result.features);
} }
} catch (error) { } catch (error) {
log.error("Failed to load membership", error, { userId: user.id }); log.error("Failed to load membership", error, { userId: user.id });
if (isMounted) { if (isMounted) {
setMembershipType("basic"); setMembershipType("basic");
setFeatures(BASIC_FEATURES);
} }
} finally { } finally {
if (isMounted) { if (isMounted) {
@ -62,7 +70,7 @@ export function useMembership(): UseMembershipResult {
return { return {
membershipType, membershipType,
features: getMembershipFeatures(membershipType) || BASIC_FEATURES, features,
loading, loading,
}; };
} }