feat(frontend): read user profile + plan from V2 Identity instead of Supabase
getUserProfile now calls the gateway /v1/users/me and /v1/users/me/plan with the access-token cookie, mapping plan_code → PlanId. Falls back to a free-plan profile when signed out or Identity is unreachable. Stripe ids drop to null (V2 billing runs through the payments service). Signature unchanged so the dashboard plan badge + settings call sites are untouched. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+70
-34
@@ -1,5 +1,6 @@
|
|||||||
|
import { gatewayUrl } from "@/lib/api/gateway";
|
||||||
|
import { getAccessToken } from "@/lib/auth/session";
|
||||||
import type { PlanId } from "@/lib/plans";
|
import type { PlanId } from "@/lib/plans";
|
||||||
import { createClient } from "@/lib/supabase/server";
|
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -10,45 +11,80 @@ export interface UserProfile {
|
|||||||
stripe_subscription_id: string | null;
|
stripe_subscription_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── V2 identity response shapes (snake_case JSON) ────────────────────────────
|
||||||
|
|
||||||
|
interface V2User {
|
||||||
|
id: string;
|
||||||
|
email?: string | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface V2UserPlan {
|
||||||
|
id: string;
|
||||||
|
plan_id: string;
|
||||||
|
plan_code: string;
|
||||||
|
plan_name: string;
|
||||||
|
initial_seconds_charge: number;
|
||||||
|
remain_charge_sec: number;
|
||||||
|
monthly_renders_used: number;
|
||||||
|
starts_at: string;
|
||||||
|
expires_at: string;
|
||||||
|
cancelled_at?: string | null;
|
||||||
|
auto_renew: boolean;
|
||||||
|
billing_period?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackProfile(userId: string, email: string | null = null): UserProfile {
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
email,
|
||||||
|
plan: "free",
|
||||||
|
billing_period: null,
|
||||||
|
// V2 billing runs through the payments service (ZarinPal/Stripe); Stripe ids
|
||||||
|
// are no longer surfaced on the profile. Kept null for shape compatibility.
|
||||||
|
stripe_customer_id: null,
|
||||||
|
stripe_subscription_id: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlan(code: string | null | undefined): PlanId {
|
||||||
|
const c = (code ?? "").toLowerCase();
|
||||||
|
return c === "pro" || c === "business" ? c : "free";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the current user's profile + plan from the Identity service via the
|
||||||
|
* gateway, authenticated with the access-token cookie. The `userId` argument is
|
||||||
|
* retained for call-site compatibility but the gateway derives identity from the
|
||||||
|
* JWT. Degrades to a free-plan fallback when signed out or the service is down.
|
||||||
|
*/
|
||||||
export async function getUserProfile(userId: string): Promise<UserProfile> {
|
export async function getUserProfile(userId: string): Promise<UserProfile> {
|
||||||
const supabase = await createClient();
|
const token = await getAccessToken();
|
||||||
|
if (!token) return fallbackProfile(userId);
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const authHeaders = {
|
||||||
.from("profiles")
|
Accept: "application/json",
|
||||||
.select("id, email, plan, billing_period, stripe_customer_id, stripe_subscription_id")
|
Authorization: `Bearer ${token}`,
|
||||||
.eq("id", userId)
|
};
|
||||||
.maybeSingle();
|
|
||||||
|
try {
|
||||||
|
const [userRes, planRes] = await Promise.all([
|
||||||
|
fetch(gatewayUrl("/v1/users/me"), { cache: "no-store", headers: authHeaders }),
|
||||||
|
fetch(gatewayUrl("/v1/users/me/plan"), { cache: "no-store", headers: authHeaders }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userRes.ok ? ((await userRes.json().catch(() => null)) as V2User | null) : null;
|
||||||
|
const plan = planRes.ok ? ((await planRes.json().catch(() => null)) as V2UserPlan | null) : null;
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
return {
|
||||||
id: userId,
|
id: user?.id ?? userId,
|
||||||
email: null,
|
email: user?.email ?? null,
|
||||||
plan: "free",
|
plan: normalizePlan(plan?.plan_code),
|
||||||
billing_period: null,
|
billing_period: plan?.billing_period ?? null,
|
||||||
stripe_customer_id: null,
|
stripe_customer_id: null,
|
||||||
stripe_subscription_id: null,
|
stripe_subscription_id: null,
|
||||||
};
|
};
|
||||||
|
} catch {
|
||||||
|
return fallbackProfile(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return {
|
|
||||||
id: userId,
|
|
||||||
email: null,
|
|
||||||
plan: "free",
|
|
||||||
billing_period: null,
|
|
||||||
stripe_customer_id: null,
|
|
||||||
stripe_subscription_id: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const plan = data.plan as PlanId;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
email: data.email,
|
|
||||||
plan: plan === "pro" || plan === "business" ? plan : "free",
|
|
||||||
billing_period: data.billing_period,
|
|
||||||
stripe_customer_id: data.stripe_customer_id,
|
|
||||||
stripe_subscription_id: data.stripe_subscription_id,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user