From b1bf5423958373ee9e9945cb1606ba28b3a7d2e1 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Thu, 26 Mar 2026 14:29:43 -0700 Subject: [PATCH 1/3] save Signed-off-by: Yujong Lee --- apps/api/src/main.rs | 4 +- apps/api/src/rate_limit.rs | 2 +- apps/desktop/src/ai/hooks/useLLMConnection.ts | 10 ++-- apps/desktop/src/auth/billing.tsx | 6 +-- .../components/oauth/provider-content.tsx | 8 ++-- .../src/calendar/components/sidebar.tsx | 6 +-- .../src/onboarding/account/after-login.tsx | 4 +- apps/desktop/src/onboarding/account/trial.tsx | 8 ++-- apps/desktop/src/onboarding/calendar.tsx | 8 ++-- .../desktop/src/settings/ai/llm/configure.tsx | 8 ++-- apps/desktop/src/settings/ai/llm/select.tsx | 6 +-- .../src/settings/ai/shared/eligibility.ts | 4 +- .../settings/ai/shared/hypr-cloud-button.tsx | 12 ++--- apps/desktop/src/settings/ai/shared/index.tsx | 4 +- .../desktop/src/settings/ai/stt/configure.tsx | 8 ++-- apps/desktop/src/settings/ai/stt/select.tsx | 6 +-- apps/desktop/src/sidebar/index.tsx | 6 ++- apps/desktop/src/stt/useSTTConnection.ts | 4 +- apps/web/src/env.ts | 1 + apps/web/src/functions/billing.ts | 18 +++++-- .../_view/app/-account-integrations.tsx | 13 +++-- .../routes/_view/app/-account-settings.tsx | 9 ++-- .../app/-integrations-upgrade-prompt.tsx | 8 +--- apps/web/src/routes/_view/app/checkout.tsx | 3 +- apps/web/src/routes/_view/app/integration.tsx | 2 +- apps/web/src/routes/_view/pricing.tsx | 38 ++++++++++++--- crates/api-auth/src/lib.rs | 40 +++++++++++++--- crates/supabase-auth/src/claims.rs | 48 +++++++++++++++++++ crates/supabase-auth/src/server/mod.rs | 18 +++++++ packages/supabase/src/billing.ts | 28 +++++++++-- 30 files changed, 245 insertions(+), 95 deletions(-) diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index 6d6d255438..bddebf68b0 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -110,8 +110,8 @@ async fn app() -> Router { ) .build(); - let auth_state_pro = - AuthState::new(&env.supabase.supabase_url).with_required_entitlement("hyprnote_pro"); + let auth_state_pro = AuthState::new(&env.supabase.supabase_url) + .with_required_entitlements(vec!["hyprnote_pro".into(), "hyprnote_lite".into()]); let auth_state_basic = AuthState::new(&env.supabase.supabase_url); let auth_state_support = AuthState::new(&env.supabase.supabase_url); diff --git a/apps/api/src/rate_limit.rs b/apps/api/src/rate_limit.rs index 8e8b869ddb..543ca6f457 100644 --- a/apps/api/src/rate_limit.rs +++ b/apps/api/src/rate_limit.rs @@ -66,7 +66,7 @@ pub async fn rate_limit( } if let Some(auth) = request.extensions().get::() { - let limiter = if auth.claims.is_pro() { + let limiter = if auth.claims.is_paid() { &state.limiter_pro } else { &state.limiter_free diff --git a/apps/desktop/src/ai/hooks/useLLMConnection.ts b/apps/desktop/src/ai/hooks/useLLMConnection.ts index 09aa79ded5..2fd2fc7253 100644 --- a/apps/desktop/src/ai/hooks/useLLMConnection.ts +++ b/apps/desktop/src/ai/hooks/useLLMConnection.ts @@ -97,11 +97,11 @@ export const useLLMConnection = (): LLMConnectionResult => { modelId: current_llm_model, providerConfig, session: auth?.session, - isPro: billing.isPro, + isPaid: billing.isPaid, }), [ auth, - billing.isPro, + billing.isPaid, current_llm_model, current_llm_provider, providerConfig, @@ -119,14 +119,14 @@ const resolveLLMConnection = (params: { modelId: string | undefined; providerConfig: AIProviderStorage | undefined; session: { access_token: string } | null | undefined; - isPro: boolean; + isPaid: boolean; }): LLMConnectionResult => { const { providerId: rawProviderId, modelId, providerConfig, session, - isPro, + isPaid, } = params; if (!rawProviderId) { @@ -166,7 +166,7 @@ const resolveLLMConnection = (params: { const context: ProviderEligibilityContext = { isAuthenticated: !!session, - isPro, + isPaid, config: { base_url: baseUrl, api_key: apiKey }, }; diff --git a/apps/desktop/src/auth/billing.tsx b/apps/desktop/src/auth/billing.tsx index 132cd6f914..0a3d69247e 100644 --- a/apps/desktop/src/auth/billing.tsx +++ b/apps/desktop/src/auth/billing.tsx @@ -60,7 +60,7 @@ export function BillingProvider({ children }: { children: ReactNode }) { const isReady = !claimsQuery.isPending; const canTrialQuery = useQuery({ - enabled: !!auth?.session && !billing.isPro, + enabled: !!auth?.session && !billing.isPaid, queryKey: [auth?.session?.user.id ?? "", "canStartTrial"], queryFn: async () => { const headers = auth?.getHeaders(); @@ -78,10 +78,10 @@ export function BillingProvider({ children }: { children: ReactNode }) { const canStartTrial = useMemo( () => ({ - data: billing.isPro ? false : (canTrialQuery.data ?? false), + data: billing.isPaid ? false : (canTrialQuery.data ?? false), isPending: canTrialQuery.isPending, }), - [billing.isPro, canTrialQuery.data, canTrialQuery.isPending], + [billing.isPaid, canTrialQuery.data, canTrialQuery.isPending], ); const upgradeToPro = useCallback(async () => { diff --git a/apps/desktop/src/calendar/components/oauth/provider-content.tsx b/apps/desktop/src/calendar/components/oauth/provider-content.tsx index b0d5cf7bbe..a219533659 100644 --- a/apps/desktop/src/calendar/components/oauth/provider-content.tsx +++ b/apps/desktop/src/calendar/components/oauth/provider-content.tsx @@ -22,8 +22,8 @@ import { buildWebAppUrl } from "~/shared/utils"; export function OAuthProviderContent({ config }: { config: CalendarProvider }) { const auth = useAuth(); - const { isPro, upgradeToPro } = useBillingAccess(); - const { data: connections, isError } = useConnections(isPro); + const { isPaid, upgradeToPro } = useBillingAccess(); + const { data: connections, isError } = useConnections(isPaid); const providerConnections = useMemo( () => connections?.filter( @@ -57,14 +57,14 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) { ); } - if (!isPro) { + if (!isPaid) { return (
); diff --git a/apps/desktop/src/calendar/components/sidebar.tsx b/apps/desktop/src/calendar/components/sidebar.tsx index 7008cd5ce2..82dd175d78 100644 --- a/apps/desktop/src/calendar/components/sidebar.tsx +++ b/apps/desktop/src/calendar/components/sidebar.tsx @@ -78,8 +78,8 @@ function ProviderAccordionItem({ calendar: ReturnType; }) { const auth = useAuth(); - const { isPro } = useBillingAccess(); - const { data: connections, isPending, isError } = useConnections(isPro); + const { isPaid } = useBillingAccess(); + const { data: connections, isPending, isError } = useConnections(isPaid); const providerConnections = connections?.filter( (connection) => connection.integration_id === provider.nangoIntegrationId, @@ -87,7 +87,7 @@ function ProviderAccordionItem({ const canAddAccount = !!provider.nangoIntegrationId && !!auth.session && - isPro && + isPaid && !isPending && !isError; const shouldConnectOnClick = diff --git a/apps/desktop/src/onboarding/account/after-login.tsx b/apps/desktop/src/onboarding/account/after-login.tsx index 76926c7960..c92b2d1191 100644 --- a/apps/desktop/src/onboarding/account/after-login.tsx +++ b/apps/desktop/src/onboarding/account/after-login.tsx @@ -19,8 +19,8 @@ function TrialStatusDisplay({ trialPhase }: { trialPhase: TrialPhase }) { )} - {trialPhase === "already-pro" && ( - + {trialPhase === "already-paid" && ( + )} {trialPhase === "already-trialing" && ( diff --git a/apps/desktop/src/onboarding/account/trial.tsx b/apps/desktop/src/onboarding/account/trial.tsx index aca2108268..bcb2928037 100644 --- a/apps/desktop/src/onboarding/account/trial.tsx +++ b/apps/desktop/src/onboarding/account/trial.tsx @@ -16,7 +16,7 @@ import * as settings from "~/store/tinybase/store/settings"; export type TrialPhase = | "checking" | "starting" - | "already-pro" + | "already-paid" | "already-trialing" | { done: StartTrialReason | "error" }; @@ -60,11 +60,11 @@ export function useTrialFlow(onContinue: () => void) { useEffect(() => { if (!auth?.session || !billing.isReady || hasTriggeredRef.current) return; - if (billing.isPro && !billing.isTrialing) { + if (billing.isPaid && !billing.isTrialing) { hasTriggeredRef.current = true; void analyticsCommands.event({ event: "trial_flow_skipped", - properties: { reason: "already_pro" }, + properties: { reason: "already_paid" }, }); if (store) configureProSettings(store); setTimeout(onContinue, 1500); @@ -89,7 +89,7 @@ export function useTrialFlow(onContinue: () => void) { if (!auth?.session) return null; if (!billing.isReady) return "checking" as const; - if (billing.isPro && !billing.isTrialing) return "already-pro" as const; + if (billing.isPaid && !billing.isTrialing) return "already-paid" as const; if (billing.isTrialing) return "already-trialing" as const; if (mutation.isPending) return "starting" as const; diff --git a/apps/desktop/src/onboarding/calendar.tsx b/apps/desktop/src/onboarding/calendar.tsx index fffa6f13ef..4c27fa04c6 100644 --- a/apps/desktop/src/onboarding/calendar.tsx +++ b/apps/desktop/src/onboarding/calendar.tsx @@ -244,8 +244,8 @@ function addIntegrationMenus({ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) { const auth = useAuth(); - const { isPro, isReady, upgradeToPro } = useBillingAccess(); - const { data: connections, isPending, isError } = useConnections(isPro); + const { isPaid, isReady, upgradeToPro } = useBillingAccess(); + const { data: connections, isPending, isError } = useConnections(isPaid); const providerConnections = useMemo( () => connections?.filter( @@ -261,7 +261,7 @@ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) { return; } - if (!isPro) { + if (!isPaid) { upgradeToPro(); return; } @@ -271,7 +271,7 @@ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) { undefined, "connect", ); - }, [auth.session, isPro, onSignIn, upgradeToPro]); + }, [auth.session, isPaid, onSignIn, upgradeToPro]); if (!GOOGLE_PROVIDER) { return null; diff --git a/apps/desktop/src/settings/ai/llm/configure.tsx b/apps/desktop/src/settings/ai/llm/configure.tsx index 1d1082e404..772d37a96d 100644 --- a/apps/desktop/src/settings/ai/llm/configure.tsx +++ b/apps/desktop/src/settings/ai/llm/configure.tsx @@ -103,7 +103,7 @@ function HyprProviderCard({ } function HyprProviderAutoRow({ highlight }: { highlight?: boolean }) { - const { isPro, canStartTrial, upgradeToPro } = useBillingAccess(); + const { isPaid, canStartTrial, upgradeToPro } = useBillingAccess(); const handleSelectProvider = settings.UI.useSetValueCallback( "current_llm_provider", @@ -120,13 +120,13 @@ function HyprProviderAutoRow({ highlight }: { highlight?: boolean }) { ); const handleClick = useCallback(() => { - if (!isPro) { + if (!isPaid) { upgradeToPro(); } else { handleSelectProvider("hyprnote"); handleSelectModel("Auto"); } - }, [isPro, upgradeToPro, handleSelectProvider, handleSelectModel]); + }, [isPaid, upgradeToPro, handleSelectProvider, handleSelectModel]); return ( @@ -137,7 +137,7 @@ function HyprProviderAutoRow({ highlight }: { highlight?: boolean }) {

{ - if (provider === "hyprnote" && !billing.isPro) { + if (provider === "hyprnote" && !billing.isPaid) { billing.upgradeToPro(); return; } @@ -186,7 +186,7 @@ export function SelectProviderAndModel() { provider.requirements, "pro", ); - const locked = requiresPro && !billing.isPro; + const locked = requiresPro && !billing.isPaid; return ( { const eligible = getProviderSelectionBlockers(provider.requirements, { isAuthenticated: !!auth?.session, - isPro: billing.isPro, + isPaid: billing.isPaid, config: { base_url: baseUrl, api_key: apiKey }, }).length === 0; diff --git a/apps/desktop/src/settings/ai/shared/eligibility.ts b/apps/desktop/src/settings/ai/shared/eligibility.ts index e398106c32..85037feff0 100644 --- a/apps/desktop/src/settings/ai/shared/eligibility.ts +++ b/apps/desktop/src/settings/ai/shared/eligibility.ts @@ -33,7 +33,7 @@ export function getRequiredConfigFields( export type ProviderEligibilityContext = { isAuthenticated: boolean; - isPro: boolean; + isPaid: boolean; config?: { base_url?: string; api_key?: string }; }; @@ -51,7 +51,7 @@ export function getProviderSelectionBlockers( } break; case "requires_entitlement": - if (req.entitlement === "pro" && !context.isPro) { + if (req.entitlement === "pro" && !context.isPaid) { blockers.push({ code: "requires_entitlement", entitlement: "pro" }); } break; diff --git a/apps/desktop/src/settings/ai/shared/hypr-cloud-button.tsx b/apps/desktop/src/settings/ai/shared/hypr-cloud-button.tsx index 06f62a0872..e7974c50a2 100644 --- a/apps/desktop/src/settings/ai/shared/hypr-cloud-button.tsx +++ b/apps/desktop/src/settings/ai/shared/hypr-cloud-button.tsx @@ -14,23 +14,23 @@ export function HyprProviderRow({ children }: { children: React.ReactNode }) { } export function HyprCloudCTAButton({ - isPro, + isPaid, canStartTrial, highlight, onClick, }: { - isPro: boolean; + isPaid: boolean; canStartTrial: boolean | undefined; highlight?: boolean; onClick: () => void; }) { - const buttonLabel = isPro + const buttonLabel = isPaid ? "Ready to use" : canStartTrial ? "Start Free Trial" - : "Upgrade to Pro"; + : "Upgrade"; - const showShimmer = highlight && !isPro; + const showShimmer = highlight && !isPaid; return (