diff --git a/apps/desktop/src/audio-player/provider.tsx b/apps/desktop/src/audio-player/provider.tsx index 509e7bd8c2..e53ed4867c 100644 --- a/apps/desktop/src/audio-player/provider.tsx +++ b/apps/desktop/src/audio-player/provider.tsx @@ -14,6 +14,8 @@ import WaveSurfer from "wavesurfer.js"; import { commands as fsSyncCommands } from "@hypr/plugin-fs-sync"; +import { useBillingAccess } from "~/auth/billing"; + type AudioPlayerState = "playing" | "paused" | "stopped"; interface TimeSnapshot { @@ -102,6 +104,7 @@ export function AudioPlayerProvider({ children: ReactNode; }) { const queryClient = useQueryClient(); + const { isPro } = useBillingAccess(); const [container, setContainer] = useState(null); const [wavesurfer, setWavesurfer] = useState(null); const [state, setState] = useState("stopped"); @@ -257,14 +260,27 @@ export function AudioPlayerProvider({ const setPlaybackRate = useCallback( (rate: number) => { + if (!isPro && rate !== 1) { + return; + } if (wavesurfer) { wavesurfer.setPlaybackRate(rate); } setPlaybackRateState(rate); }, - [wavesurfer], + [isPro, wavesurfer], ); + useEffect(() => { + if (isPro || playbackRate === 1) { + return; + } + if (wavesurfer) { + wavesurfer.setPlaybackRate(1); + } + setPlaybackRateState(1); + }, [isPro, playbackRate, wavesurfer]); + const deleteRecordingMutation = useMutation({ mutationFn: async () => { const result = await fsSyncCommands.audioDelete(sessionId); diff --git a/apps/desktop/src/audio-player/timeline.tsx b/apps/desktop/src/audio-player/timeline.tsx index 56b831bf27..053c7c63c5 100644 --- a/apps/desktop/src/audio-player/timeline.tsx +++ b/apps/desktop/src/audio-player/timeline.tsx @@ -5,11 +5,13 @@ import { cn } from "@hypr/utils"; import { useAudioPlayer, useAudioTime } from "./provider"; +import { useBillingAccess } from "~/auth/billing"; import { useNativeContextMenu } from "~/shared/hooks/useNativeContextMenu"; const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]; export function Timeline() { + const { isPro } = useBillingAccess(); const { registerContainer, state, @@ -116,49 +118,51 @@ export function Timeline() { {formatTime(time.total)} -
- - {showRateMenu && ( -
+ - ))} -
- )} -
+ {playbackRate}x + + {showRateMenu && ( +
+ {PLAYBACK_RATES.map((rate) => ( + + ))} +
+ )} + + ) : null}
{ @@ -49,6 +52,8 @@ const BillingContext = createContext(null); export function BillingProvider({ children }: { children: ReactNode }) { const auth = useAuth(); + const settingsStore = settings.UI.useStore(settings.STORE_ID); + const { current_llm_provider } = settings.UI.useValues(settings.STORE_ID); const claimsQuery = useQuery({ queryKey: ["tokenInfo", auth?.session?.access_token ?? ""], @@ -89,6 +94,25 @@ export function BillingProvider({ children }: { children: ReactNode }) { void openerCommands.openUrl(url, null); }, []); + useEffect(() => { + if (!auth?.session?.user.id || !isReady || billing.isPaid) { + return; + } + + if (current_llm_provider !== "hyprnote") { + return; + } + + settingsStore?.setValue("current_llm_provider", ""); + settingsStore?.setValue("current_llm_model", ""); + }, [ + auth?.session?.user.id, + billing.isPaid, + current_llm_provider, + isReady, + settingsStore, + ]); + const value = useMemo( () => ({ ...billing, diff --git a/apps/desktop/src/calendar/components/oauth/provider-content.tsx b/apps/desktop/src/calendar/components/oauth/provider-content.tsx index 8095f20c14..d94c907953 100644 --- a/apps/desktop/src/calendar/components/oauth/provider-content.tsx +++ b/apps/desktop/src/calendar/components/oauth/provider-content.tsx @@ -21,8 +21,8 @@ import { openIntegrationUrl } from "~/shared/integration"; export function OAuthProviderContent({ config }: { config: CalendarProvider }) { const auth = useAuth(); - const { isPaid, upgradeToPro } = useBillingAccess(); - const { data: connections, isError } = useConnections(isPaid); + const { isPro, upgradeToPro } = useBillingAccess(); + const { data: connections, isError } = useConnections(isPro); const providerConnections = useMemo( () => connections?.filter( @@ -62,7 +62,7 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) { ); } - if (!isPaid) { + if (!isPro) { return (
+
+
+ ); +} + function TabContentFolderTopLevel() { return (
diff --git a/apps/desktop/src/onboarding/calendar.tsx b/apps/desktop/src/onboarding/calendar.tsx index 4c27fa04c6..fffa6f13ef 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 { isPaid, isReady, upgradeToPro } = useBillingAccess(); - const { data: connections, isPending, isError } = useConnections(isPaid); + const { isPro, isReady, upgradeToPro } = useBillingAccess(); + const { data: connections, isPending, isError } = useConnections(isPro); const providerConnections = useMemo( () => connections?.filter( @@ -261,7 +261,7 @@ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) { return; } - if (!isPaid) { + if (!isPro) { upgradeToPro(); return; } @@ -271,7 +271,7 @@ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) { undefined, "connect", ); - }, [auth.session, isPaid, onSignIn, upgradeToPro]); + }, [auth.session, isPro, onSignIn, upgradeToPro]); if (!GOOGLE_PROVIDER) { return null; diff --git a/apps/desktop/src/session/components/note-input/enhanced/config-error.tsx b/apps/desktop/src/session/components/note-input/enhanced/config-error.tsx index f6f8cb6453..f1ecd8f4c0 100644 --- a/apps/desktop/src/session/components/note-input/enhanced/config-error.tsx +++ b/apps/desktop/src/session/components/note-input/enhanced/config-error.tsx @@ -44,6 +44,10 @@ function getMessageForStatus(status: LLMConnectionStatus): string { return "You need to sign in to use Char's language model"; } + if (status.status === "error" && status.reason === "not_pro") { + return "Your Char plan has expired. Configure another language model or renew your plan"; + } + if (status.status === "error" && status.reason === "missing_config") { const missing = status.missing; if (missing.includes("api_key") && missing.includes("base_url")) { diff --git a/apps/desktop/src/session/components/note-input/enhanced/index.tsx b/apps/desktop/src/session/components/note-input/enhanced/index.tsx index 985690eeff..7b0821c0f0 100644 --- a/apps/desktop/src/session/components/note-input/enhanced/index.tsx +++ b/apps/desktop/src/session/components/note-input/enhanced/index.tsx @@ -32,6 +32,7 @@ export const Enhanced = forwardRef< llmStatus.status === "pending" || (llmStatus.status === "error" && (llmStatus.reason === "missing_config" || + llmStatus.reason === "not_pro" || llmStatus.reason === "unauthenticated")); if (status === "idle" && isConfigError && !hasContent) { diff --git a/apps/desktop/src/session/components/note-input/header.tsx b/apps/desktop/src/session/components/note-input/header.tsx index 5fbe57c5fd..dd3171dba0 100644 --- a/apps/desktop/src/session/components/note-input/header.tsx +++ b/apps/desktop/src/session/components/note-input/header.tsx @@ -1083,6 +1083,7 @@ function useEnhanceLogic(sessionId: string, enhancedNoteId: string) { llmStatus.status === "pending" || (llmStatus.status === "error" && (llmStatus.reason === "missing_config" || + llmStatus.reason === "not_pro" || llmStatus.reason === "unauthenticated")); const isIdleWithConfigError = enhanceTask.isIdle && isConfigError; diff --git a/apps/desktop/src/session/components/outer-header/folder/index.tsx b/apps/desktop/src/session/components/outer-header/folder/index.tsx index f73768e61d..3044e91b17 100644 --- a/apps/desktop/src/session/components/outer-header/folder/index.tsx +++ b/apps/desktop/src/session/components/outer-header/folder/index.tsx @@ -12,12 +12,14 @@ import { Button } from "@hypr/ui/components/ui/button"; import { SearchableFolderDropdown } from "./searchable-dropdown"; +import { useBillingAccess } from "~/auth/billing"; import { FolderBreadcrumb } from "~/shared/ui/folder-breadcrumb"; import * as main from "~/store/tinybase/store/main"; import { useSessionTitle } from "~/store/zustand/live-title"; import { useTabs } from "~/store/zustand/tabs"; export function FolderChain({ sessionId }: { sessionId: string }) { + const { isPro } = useBillingAccess(); const folderId = main.UI.useCell( "sessions", sessionId, @@ -40,6 +42,20 @@ export function FolderChain({ sessionId }: { sessionId: string }) { main.STORE_ID, ); + if (!isPro) { + return ( + + + + + + + + + + ); + } + return ( diff --git a/apps/desktop/src/session/components/outer-header/folder/searchable-dropdown.tsx b/apps/desktop/src/session/components/outer-header/folder/searchable-dropdown.tsx index 34019522ae..d70336dfaf 100644 --- a/apps/desktop/src/session/components/outer-header/folder/searchable-dropdown.tsx +++ b/apps/desktop/src/session/components/outer-header/folder/searchable-dropdown.tsx @@ -17,6 +17,7 @@ import { DropdownMenuTrigger, } from "@hypr/ui/components/ui/dropdown-menu"; +import { useBillingAccess } from "~/auth/billing"; import { sessionOps } from "~/store/tinybase/persister/session/ops"; import * as main from "~/store/tinybase/store/main"; import { useListener } from "~/stt/contexts"; @@ -201,8 +202,13 @@ function useSessionFolderId(sessionId: string) { } function useMoveDisabledReason(sessionId: string) { + const { isPro } = useBillingAccess(); const sessionMode = useListener((state) => state.getSessionMode(sessionId)); + if (!isPro) { + return "Upgrade to Pro to move notes into folders."; + } + if (sessionMode === "active" || sessionMode === "finalizing") { return "Stop listening before moving this note."; } diff --git a/apps/desktop/src/session/components/outer-header/overflow/index.tsx b/apps/desktop/src/session/components/outer-header/overflow/index.tsx index 0f36bea161..52eb21454b 100644 --- a/apps/desktop/src/session/components/outer-header/overflow/index.tsx +++ b/apps/desktop/src/session/components/outer-header/overflow/index.tsx @@ -17,6 +17,7 @@ import { Listening } from "./listening"; import { Copy, Folder, ShowInFinder } from "./misc"; import { useAudioPlayer } from "~/audio-player"; +import { useBillingAccess } from "~/auth/billing"; import { useHasTranscript } from "~/session/components/shared"; import type { EditorView } from "~/store/zustand/tabs/schema"; @@ -31,6 +32,7 @@ export function OverflowButton({ const [isExportModalOpen, setIsExportModalOpen] = useState(false); const hasTranscript = useHasTranscript(sessionId); const { audioExists } = useAudioPlayer(); + const { isPro } = useBillingAccess(); const openExportModal = () => { setOpen(false); requestAnimationFrame(() => setIsExportModalOpen(true)); @@ -51,7 +53,7 @@ export function OverflowButton({ - + {isPro && } , baseUrl: new URL("/llm", env.VITE_API_URL).toString(), - requirements: [{ kind: "requires_auth" }], + requirements: [ + { kind: "requires_auth" }, + { kind: "requires_entitlement", entitlement: "pro" }, + ], }, { id: "lmstudio", diff --git a/apps/desktop/src/shared/main/index.tsx b/apps/desktop/src/shared/main/index.tsx index 6eaa5d601f..36bd64d1ed 100644 --- a/apps/desktop/src/shared/main/index.tsx +++ b/apps/desktop/src/shared/main/index.tsx @@ -25,6 +25,7 @@ import { cn } from "@hypr/utils"; import { TabContentEmpty, TabItemEmpty } from "./empty"; import { useNewNote, useNewNoteAndListen } from "./useNewNote"; +import { useBillingAccess } from "~/auth/billing"; import { TabContentCalendar, TabItemCalendar } from "~/calendar"; import { TabContentChangelog, TabItemChangelog } from "~/changelog"; import { ChatFloatingButton } from "~/chat/components/floating-button"; @@ -907,6 +908,7 @@ function useTabsShortcuts() { const liveSessionId = useListener((state) => state.live.sessionId); const liveStatus = useListener((state) => state.live.status); const isListening = liveStatus === "active" || liveStatus === "finalizing"; + const { isPro } = useBillingAccess(); const { chat } = useShell(); const newNote = useNewNote(); @@ -1068,13 +1070,19 @@ function useTabsShortcuts() { useHotkeys( "mod+shift+l", - () => openNew({ type: "folders", id: null }), + () => { + if (!isPro) { + return; + } + + openNew({ type: "folders", id: null }); + }, { preventDefault: true, enableOnFormTags: true, enableOnContentEditable: true, }, - [openNew], + [isPro, openNew], ); const newNoteAndListen = useNewNoteAndListen(); diff --git a/apps/desktop/src/sidebar/profile/index.tsx b/apps/desktop/src/sidebar/profile/index.tsx index 7994e91ef0..a473ccf5fd 100644 --- a/apps/desktop/src/sidebar/profile/index.tsx +++ b/apps/desktop/src/sidebar/profile/index.tsx @@ -20,6 +20,7 @@ import { NotificationsMenuContent } from "./notification"; import { MenuItem } from "./shared"; import { useAuth } from "~/auth"; +import { useBillingAccess } from "~/auth/billing"; import { useAutoCloser } from "~/shared/hooks/useAutoCloser"; import * as main from "~/store/tinybase/store/main"; import { useTabs } from "~/store/zustand/tabs"; @@ -38,6 +39,7 @@ export function ProfileSection({ onExpandChange }: ProfileSectionProps = {}) { const openNew = useTabs((state) => state.openNew); const transitionChatMode = useTabs((state) => state.transitionChatMode); const auth = useAuth(); + const { isPro } = useBillingAccess(); const isAuthenticated = !!auth?.session; @@ -146,12 +148,16 @@ export function ProfileSection({ onExpandChange }: ProfileSectionProps = {}) { ]); const menuItems = [ - { - icon: FolderOpenIcon, - label: "Folders", - onClick: handleClickFolders, - badge: ⌘ ⇧ L, - }, + ...(isPro + ? [ + { + icon: FolderOpenIcon, + label: "Folders", + onClick: handleClickFolders, + badge: ⌘ ⇧ L, + }, + ] + : []), { icon: UsersIcon, label: "Contacts",