From a8f1bd6e74734cc5d6b07edcd1a62be9cfde0596 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:40:32 +0000 Subject: [PATCH 1/3] fix: harmonize account settings between browser and desktop app - Add subscription_status and trial_end claims to JWT via auth hook - Update desktop billing context to extract and expose subscription status - Update account settings UI to show 'TRIAL' for trialing users instead of 'PRO' - Add trial end date display when available - Export SubscriptionStatus type from @hypr/supabase package This fixes the discrepancy where browser showed 'Trial' but desktop showed 'PRO' for users on trial subscriptions. Co-Authored-By: john@hyprnote.com --- apps/desktop/src/billing.tsx | 64 +++++++++++++++- .../components/settings/general/account.tsx | 18 ++++- packages/supabase/src/index.ts | 2 +- packages/supabase/src/jwt.ts | 12 +++ ...2000001_add_subscription_claims_to_jwt.sql | 73 +++++++++++++++++++ 5 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 supabase/migrations/20250122000001_add_subscription_claims_to_jwt.sql diff --git a/apps/desktop/src/billing.tsx b/apps/desktop/src/billing.tsx index bb01cd3a47..dc3c932ff0 100644 --- a/apps/desktop/src/billing.tsx +++ b/apps/desktop/src/billing.tsx @@ -11,23 +11,53 @@ import { import { getRpcCanStartTrial } from "@hypr/api-client"; import { createClient } from "@hypr/api-client/client"; import { commands as openerCommands } from "@hypr/plugin-opener2"; +import type { SubscriptionStatus } from "@hypr/supabase"; import { useAuth } from "./auth"; import { env } from "./env"; import { getScheme } from "./utils"; +type JwtClaims = { + entitlements?: string[]; + subscription_status?: SubscriptionStatus; + trial_end?: number; +}; + export function getEntitlementsFromToken(accessToken: string): string[] { try { - const decoded = jwtDecode<{ entitlements?: string[] }>(accessToken); + const decoded = jwtDecode(accessToken); return decoded.entitlements ?? []; } catch { return []; } } +export function getSubscriptionStatusFromToken( + accessToken: string, +): SubscriptionStatus | undefined { + try { + const decoded = jwtDecode(accessToken); + return decoded.subscription_status; + } catch { + return undefined; + } +} + +export function getTrialEndFromToken(accessToken: string): number | undefined { + try { + const decoded = jwtDecode(accessToken); + return decoded.trial_end; + } catch { + return undefined; + } +} + type BillingContextValue = { entitlements: string[]; isPro: boolean; + subscriptionStatus: SubscriptionStatus; + isOnTrial: boolean; + trialEnd: number | undefined; canStartTrial: boolean; upgradeToPro: () => void; }; @@ -46,11 +76,30 @@ export function BillingProvider({ children }: { children: ReactNode }) { return getEntitlementsFromToken(auth.session.access_token); }, [auth?.session?.access_token]); + const subscriptionStatus = useMemo(() => { + if (!auth?.session?.access_token) { + return "none"; + } + return getSubscriptionStatusFromToken(auth.session.access_token) ?? "none"; + }, [auth?.session?.access_token]); + + const trialEnd = useMemo(() => { + if (!auth?.session?.access_token) { + return undefined; + } + return getTrialEndFromToken(auth.session.access_token); + }, [auth?.session?.access_token]); + const isPro = useMemo( () => entitlements.includes("hyprnote_pro"), [entitlements], ); + const isOnTrial = useMemo( + () => subscriptionStatus === "trialing", + [subscriptionStatus], + ); + const canTrialQuery = useQuery({ enabled: !!auth?.session && !isPro, queryKey: [auth?.session?.user.id ?? "", "canStartTrial"], @@ -82,10 +131,21 @@ export function BillingProvider({ children }: { children: ReactNode }) { () => ({ entitlements, isPro, + subscriptionStatus, + isOnTrial, + trialEnd, canStartTrial, upgradeToPro, }), - [entitlements, isPro, canStartTrial, upgradeToPro], + [ + entitlements, + isPro, + subscriptionStatus, + isOnTrial, + trialEnd, + canStartTrial, + upgradeToPro, + ], ); return ( diff --git a/apps/desktop/src/components/settings/general/account.tsx b/apps/desktop/src/components/settings/general/account.tsx index c66cde8e22..b7763ed4a4 100644 --- a/apps/desktop/src/components/settings/general/account.tsx +++ b/apps/desktop/src/components/settings/general/account.tsx @@ -19,7 +19,7 @@ const WEB_APP_BASE_URL = env.VITE_APP_URL ?? "http://localhost:3000"; export function AccountSettings() { const auth = useAuth(); - const { isPro } = useBillingAccess(); + const { isPro, isOnTrial, trialEnd } = useBillingAccess(); const store = settings.UI.useStore(settings.STORE_ID); const isAuthenticated = !!auth?.session; @@ -171,7 +171,17 @@ export function AccountSettings() { } >

@@ -191,7 +201,7 @@ export function AccountSettings() { function BillingButton() { const auth = useAuth(); - const { isPro } = useBillingAccess(); + const { isPro, isOnTrial } = useBillingAccess(); const { open: openTrialBeginModal } = useTrialBeginModal(); const canTrialQuery = useQuery({ @@ -262,7 +272,7 @@ function BillingButton() { void openerCommands.openUrl(`${WEB_APP_BASE_URL}/app/account`, null); }, []); - if (isPro) { + if (isPro || isOnTrial) { return (