From 6d2a296c38a66163f9494c1a9677ea546c947928 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sat, 30 May 2026 06:02:17 +0330 Subject: [PATCH] feat(frontend): read user profile + plan from V2 Identity instead of Supabase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/profiles.ts | 110 +++++++++++++++++++++++++++++--------------- 1 file changed, 73 insertions(+), 37 deletions(-) diff --git a/src/lib/profiles.ts b/src/lib/profiles.ts index 4819b88..dac16ba 100644 --- a/src/lib/profiles.ts +++ b/src/lib/profiles.ts @@ -1,5 +1,6 @@ +import { gatewayUrl } from "@/lib/api/gateway"; +import { getAccessToken } from "@/lib/auth/session"; import type { PlanId } from "@/lib/plans"; -import { createClient } from "@/lib/supabase/server"; export interface UserProfile { id: string; @@ -10,45 +11,80 @@ export interface UserProfile { stripe_subscription_id: string | null; } -export async function getUserProfile(userId: string): Promise { - const supabase = await createClient(); +// ── V2 identity response shapes (snake_case JSON) ──────────────────────────── - const { data, error } = await supabase - .from("profiles") - .select("id, email, plan, billing_period, stripe_customer_id, stripe_subscription_id") - .eq("id", userId) - .maybeSingle(); +interface V2User { + id: string; + email?: string | null; + [key: string]: unknown; +} - if (error) { - return { - id: userId, - email: null, - plan: "free", - billing_period: null, - stripe_customer_id: null, - stripe_subscription_id: null, - }; - } - - 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; +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: 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, + 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 { + const token = await getAccessToken(); + if (!token) return fallbackProfile(userId); + + const authHeaders = { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }; + + 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; + + return { + id: user?.id ?? userId, + email: user?.email ?? null, + plan: normalizePlan(plan?.plan_code), + billing_period: plan?.billing_period ?? null, + stripe_customer_id: null, + stripe_subscription_id: null, + }; + } catch { + return fallbackProfile(userId); + } +}