Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion apps/desktop/src/audio-player/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -102,6 +104,7 @@ export function AudioPlayerProvider({
children: ReactNode;
}) {
const queryClient = useQueryClient();
const { isPro } = useBillingAccess();
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [wavesurfer, setWavesurfer] = useState<WaveSurfer | null>(null);
const [state, setState] = useState<AudioPlayerState>("stopped");
Expand Down Expand Up @@ -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);
Expand Down
84 changes: 44 additions & 40 deletions apps/desktop/src/audio-player/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -116,49 +118,51 @@ export function Timeline() {
<span>{formatTime(time.total)}</span>
</div>

<div className="relative shrink-0" ref={rateMenuRef}>
<button
onClick={() => setShowRateMenu((prev) => !prev)}
className={cn([
"flex items-center justify-center",
"h-6 rounded-md px-1.5",
"border border-neutral-200 bg-white",
"transition-colors hover:bg-neutral-100",
"font-mono text-xs text-neutral-700 select-none",
"shadow-xs",
])}
>
{playbackRate}x
</button>
{showRateMenu && (
<div
{isPro ? (
<div className="relative shrink-0" ref={rateMenuRef}>
<button
onClick={() => setShowRateMenu((prev) => !prev)}
className={cn([
"absolute right-0 bottom-full mb-1",
"rounded-lg border border-neutral-200 bg-white shadow-md",
"z-50 py-1",
"flex items-center justify-center",
"h-6 rounded-md px-1.5",
"border border-neutral-200 bg-white",
"transition-colors hover:bg-neutral-100",
"font-mono text-xs text-neutral-700 select-none",
"shadow-xs",
])}
>
{PLAYBACK_RATES.map((rate) => (
<button
key={rate}
onClick={() => {
setPlaybackRate(rate);
setShowRateMenu(false);
}}
className={cn([
"block w-full px-3 py-1 text-left font-mono text-xs select-none",
"transition-colors hover:bg-neutral-100",
rate === playbackRate
? "font-semibold text-neutral-900"
: "text-neutral-600",
])}
>
{rate}x
</button>
))}
</div>
)}
</div>
{playbackRate}x
</button>
{showRateMenu && (
<div
className={cn([
"absolute right-0 bottom-full mb-1",
"rounded-lg border border-neutral-200 bg-white shadow-md",
"z-50 py-1",
])}
>
{PLAYBACK_RATES.map((rate) => (
<button
key={rate}
onClick={() => {
setPlaybackRate(rate);
setShowRateMenu(false);
}}
className={cn([
"block w-full px-3 py-1 text-left font-mono text-xs select-none",
"transition-colors hover:bg-neutral-100",
rate === playbackRate
? "font-semibold text-neutral-900"
: "text-neutral-600",
])}
>
{rate}x
</button>
))}
</div>
)}
</div>
) : null}

<div
ref={registerContainer}
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/src/auth/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
} from "react";

Expand All @@ -21,6 +22,8 @@ import { env } from "../env";
import { buildWebAppUrl } from "../shared/utils";
import { useAuth } from "./context";

import * as settings from "~/store/tinybase/store/settings";

async function getClaimsFromToken(
accessToken: string,
): Promise<SupabaseJwtPayload | null> {
Expand Down Expand Up @@ -49,6 +52,8 @@ const BillingContext = createContext<BillingContextValue | null>(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 ?? ""],
Expand Down Expand Up @@ -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<BillingContextValue>(
() => ({
...billing,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -62,7 +62,7 @@ export function OAuthProviderContent({ config }: { config: CalendarProvider }) {
);
}

if (!isPaid) {
if (!isPro) {
return (
<div className="pt-1 pb-2">
<button
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/folders/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { FolderIcon, FoldersIcon, StickyNoteIcon } from "lucide-react";
import { useCallback, useMemo, useState } from "react";

import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/utils";

import { Section } from "./shared";

import { useBillingAccess } from "~/auth/billing";
import { StandardTabWrapper } from "~/shared/main";
import { type TabItem, TabItemBase } from "~/shared/tabs";
import {
Expand Down Expand Up @@ -138,10 +140,20 @@ const TabItemFolderSpecific: TabItem<Extract<Tab, { type: "folders" }>> = ({
};

export function TabContentFolder({ tab }: { tab: Tab }) {
const { isPro, upgradeToPro } = useBillingAccess();

if (tab.type !== "folders") {
return null;
}

if (!isPro) {
return (
<StandardTabWrapper>
<FolderUpgradeState onUpgrade={upgradeToPro} />
</StandardTabWrapper>
);
}

return (
<StandardTabWrapper>
{tab.id === null ? (
Expand All @@ -153,6 +165,23 @@ export function TabContentFolder({ tab }: { tab: Tab }) {
);
}

function FolderUpgradeState({ onUpgrade }: { onUpgrade: () => void }) {
return (
<div className="flex h-full items-center justify-center p-8">
<div className="flex max-w-sm flex-col items-center gap-3 text-center">
<FoldersIcon className="h-10 w-10 text-neutral-500" />
<h1 className="text-lg font-semibold text-neutral-900">
Folders are available on Pro
</h1>
<p className="text-sm text-neutral-600">
Upgrade to Pro to open folder tabs and organize notes into folders.
</p>
<Button onClick={onUpgrade}>Upgrade to Pro</Button>
</div>
</div>
);
}

function TabContentFolderTopLevel() {
return (
<div className="justify-left flex h-full items-start p-8">
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/onboarding/calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -261,7 +261,7 @@ function GoogleCalendarProvider({ onSignIn }: { onSignIn: () => void }) {
return;
}

if (!isPaid) {
if (!isPro) {
upgradeToPro();
return;
}
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/session/components/note-input/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions apps/desktop/src/session/components/outer-header/folder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,6 +42,20 @@ export function FolderChain({ sessionId }: { sessionId: string }) {
main.STORE_ID,
);

if (!isPro) {
return (
<Breadcrumb className="ml-1.5 w-full min-w-0">
<BreadcrumbList className="w-full flex-nowrap gap-0.5 overflow-hidden text-xs text-neutral-700">
<BreadcrumbItem className="min-w-0 flex-1 overflow-hidden">
<BreadcrumbPage className="block w-full min-w-0">
<TitleInput title={title} handleChangeTitle={handleChangeTitle} />
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
}

return (
<Breadcrumb className="ml-1.5 w-full min-w-0">
<BreadcrumbList className="w-full flex-nowrap gap-0.5 overflow-hidden text-xs text-neutral-700">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.";
}
Expand Down
Loading
Loading