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
118 changes: 118 additions & 0 deletions apps/desktop/src/components/devtool/trial-begin-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Brain, Cloud, ExternalLink, Puzzle, Sparkle, X } from "lucide-react";
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { create } from "zustand";

import { cn } from "@hypr/utils";

type TrialBeginModalStore = {
isOpen: boolean;
open: () => void;
close: () => void;
};

export const useTrialBeginModal = create<TrialBeginModalStore>((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
}));

export function TrialBeginModal() {
const { isOpen, close } = useTrialBeginModal();

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
close();
}
};

if (isOpen) {
document.addEventListener("keydown", handleEscape);
}

return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [isOpen, close]);

if (!isOpen) {
return null;
}

return createPortal(
<>
<div
className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm"
onClick={close}
>
<div
data-tauri-drag-region
className="w-full min-h-11"
onClick={(e) => e.stopPropagation()}
/>
</div>

<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 pointer-events-none">
<div
className={cn([
"relative w-full max-w-lg max-h-full overflow-auto",
"bg-background rounded-lg shadow-lg pointer-events-auto",
])}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={close}
className="absolute right-6 top-6 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>

<div className="flex flex-col items-center gap-10 p-10 text-center">
<div className="flex flex-col gap-3 max-w-sm">
<h2 className="font-serif text-3xl font-semibold">
Welcome to Pro!
</h2>
<p className="text-muted-foreground">
You just gained access to these features
</p>
</div>

<div className="flex flex-wrap justify-center gap-2 max-w-md">
{[
{ label: "Pro AI models", icon: Sparkle },
{ label: "Cloud sync", icon: Cloud },
{ label: "Memory", icon: Brain },
{ label: "Integrations", icon: Puzzle },
{ label: "Shareable links", icon: ExternalLink },
{ label: "and more", icon: null },
].map(({ label, icon: Icon }) => (
<div
key={label}
className={cn([
"px-4 h-8 flex items-center text-sm rounded-full",
"bg-gradient-to-b from-white to-stone-50 border border-neutral-300 text-neutral-700",
"shadow-sm hover:shadow-md hover:scale-[102%] transition-all",
Icon && "gap-2",
])}
>
{Icon && <Icon className="h-4 w-4" />}
{label}
</div>
))}
</div>

<button
onClick={close}
className="px-6 py-2 rounded-full bg-gradient-to-t from-stone-600 to-stone-500 text-white text-sm font-medium transition-opacity duration-150 hover:opacity-90"
>
Let's go!
</button>
</div>
</div>
</div>
</>,
document.body,
);
}
131 changes: 85 additions & 46 deletions apps/desktop/src/components/devtool/trial-expired-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Brain, Cloud, ExternalLink, Puzzle, Sparkle, X } from "lucide-react";
import { useEffect } from "react";
import { createPortal } from "react-dom";
import { create } from "zustand";

import { Modal } from "@hypr/ui/components/ui/modal";
import { cn } from "@hypr/utils";

import { useBillingAccess } from "../../billing";
Expand All @@ -27,58 +28,96 @@ export function TrialExpiredModal() {
close();
};

return (
<Modal open={isOpen} onClose={close} preventClose size="lg">
<div className="relative flex flex-col">
<button
onClick={close}
className="absolute right-4 top-4 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
close();
}
};

<div className="flex flex-col items-center gap-8 p-6 text-center">
<div className="flex flex-col gap-3">
<h2 className="font-serif text-3xl font-semibold">
Your free trial is over
</h2>
<p className="text-muted-foreground">
You can keep using Hyprnote for free,
<br />
but here's what you'll be losing
</p>
</div>
if (isOpen) {
document.addEventListener("keydown", handleEscape);
}

<div className="flex flex-wrap justify-center gap-3 max-w-md">
{[
{ label: "Pro AI models", icon: Sparkle },
{ label: "Cloud sync", icon: Cloud },
{ label: "Memory", icon: Brain },
{ label: "Integrations", icon: Puzzle },
{ label: "Shareable links", icon: ExternalLink },
].map(({ label, icon: Icon }) => (
<div
key={label}
className={cn([
"rounded-full border border-border bg-secondary/50 px-4 py-2 text-[12px] text-secondary-foreground",
"flex items-center gap-2",
])}
>
<Icon className="h-4 w-4" />
{label}
</div>
))}
</div>
return () => {
document.removeEventListener("keydown", handleEscape);
};
}, [isOpen, close]);

if (!isOpen) {
return null;
}

return createPortal(
<>
<div className="fixed inset-0 z-[9999] bg-black/50 backdrop-blur-sm">
<div
data-tauri-drag-region
className="w-full min-h-11"
onClick={(e) => e.stopPropagation()}
/>
</div>

<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 pointer-events-none">
<div
className={cn([
"relative w-full max-w-lg max-h-full overflow-auto",
"bg-background rounded-lg shadow-lg pointer-events-auto",
])}
onClick={(e) => e.stopPropagation()}
>
<button
onClick={handleUpgrade}
className="px-6 py-2 rounded-full bg-gradient-to-t from-stone-600 to-stone-500 text-white text-sm font-medium transition-opacity duration-150 hover:opacity-90"
onClick={close}
className="absolute right-6 top-6 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
aria-label="Close"
>
I'd like to keep using <span className="font-serif">Pro</span>
<X className="h-4 w-4" />
</button>

<div className="flex flex-col items-center gap-10 p-10 text-center">
<div className="flex flex-col gap-3">
<h2 className="font-serif text-3xl font-semibold">
Your free trial is over
</h2>
<p className="text-muted-foreground">
Here's what you just lost access to
</p>
</div>

<div className="flex flex-wrap justify-center gap-2 max-w-md">
{[
{ label: "Pro AI models", icon: Sparkle },
{ label: "Cloud sync", icon: Cloud },
{ label: "Memory", icon: Brain },
{ label: "Integrations", icon: Puzzle },
{ label: "Shareable links", icon: ExternalLink },
{ label: "and more", icon: null },
].map(({ label, icon: Icon }) => (
<div
key={label}
className={cn([
"px-4 h-8 flex items-center text-sm rounded-full",
"bg-gradient-to-b from-white to-stone-50 border border-neutral-300 text-neutral-700",
"shadow-sm hover:shadow-md hover:scale-[102%] transition-all",
Icon && "gap-2",
])}
>
{Icon && <Icon className="h-4 w-4" />}
{label}
</div>
))}
</div>

<button
onClick={handleUpgrade}
className="px-6 py-2 rounded-full bg-gradient-to-t from-stone-600 to-stone-500 text-white text-sm font-medium transition-opacity duration-150 hover:opacity-90"
>
I'd like to keep using <span className="font-serif">Pro</span>
</button>
</div>
</div>
</div>
</Modal>
</>,
document.body,
);
}
2 changes: 2 additions & 0 deletions apps/desktop/src/components/main-app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AuthProvider } from "../auth";
import { BillingProvider } from "../billing";
import { NetworkProvider } from "../contexts/network";
import { useTabs } from "../store/zustand/tabs";
import { TrialBeginModal } from "./devtool/trial-begin-modal";
import { TrialExpiredModal } from "./devtool/trial-expired-modal";
import { useNewNote } from "./main/shared";

Expand All @@ -19,6 +20,7 @@ export default function MainAppLayout() {
<BillingProvider>
<NetworkProvider>
<Outlet />
<TrialBeginModal />
<TrialExpiredModal />
</NetworkProvider>
</BillingProvider>
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/components/main/sidebar/devtool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../../store/tinybase/store/main";
import { useTabs } from "../../../store/zustand/tabs";
import { type SeedDefinition, seeds } from "../../devtool/seed/index";
import { useTrialBeginModal } from "../../devtool/trial-begin-modal";
import { useTrialExpiredModal } from "../../devtool/trial-expired-modal";
import { getLatestVersion } from "../body/changelog";

Expand Down Expand Up @@ -313,11 +314,25 @@ function NavigationCard() {
}

function ModalsCard() {
const { open: openTrialBeginModal } = useTrialBeginModal();
const { open: openTrialExpiredModal } = useTrialExpiredModal();

return (
<DevtoolCard title="Modals">
<div className="flex flex-col gap-1.5">
<button
type="button"
onClick={openTrialBeginModal}
className={cn([
"w-full px-2.5 py-1.5 rounded-md",
"text-xs font-medium text-left",
"border border-neutral-200 text-neutral-700",
"cursor-pointer transition-colors",
"hover:bg-neutral-50 hover:border-neutral-300",
])}
>
Trial Begin
</button>
<button
type="button"
onClick={openTrialExpiredModal}
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/components/onboarding/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getEntitlementsFromToken } from "../../billing";
import { env } from "../../env";
import { Route } from "../../routes/app/onboarding/_layout.index";
import * as settings from "../../store/tinybase/store/settings";
import { useTrialBeginModal } from "../devtool/trial-begin-modal";
import { getBack, getNext, type StepProps } from "./config";
import { STEP_ID_CONFIGURE_NOTICE } from "./configure-notice";
import { Divider, OnboardingContainer } from "./shared";
Expand All @@ -19,6 +20,7 @@ export function Login({ onNavigate }: StepProps) {
const search = Route.useSearch();
const auth = useAuth();
const [callbackUrl, setCallbackUrl] = useState("");
const { open: openTrialBeginModal } = useTrialBeginModal();

const setLlmProvider = settings.UI.useSetValueCallback(
"current_llm_provider",
Expand Down Expand Up @@ -78,6 +80,7 @@ export function Login({ onNavigate }: StepProps) {
onSuccess: (isPro) => {
if (isPro) {
setTrialDefaults();
openTrialBeginModal();
}
const nextSearch = { ...search, pro: isPro };
onNavigate({ ...nextSearch, step: getNext(nextSearch)! });
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/components/settings/general/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { cn } from "@hypr/utils";
import { useAuth } from "../../../auth";
import { useBillingAccess } from "../../../billing";
import { env } from "../../../env";
import { useTrialBeginModal } from "../../devtool/trial-begin-modal";

const WEB_APP_BASE_URL = env.VITE_APP_URL ?? "http://localhost:3000";

Expand Down Expand Up @@ -176,6 +177,7 @@ export function AccountSettings() {
function BillingButton() {
const auth = useAuth();
const { isPro } = useBillingAccess();
const { open: openTrialBeginModal } = useTrialBeginModal();

const canTrialQuery = useQuery({
enabled: !!auth?.session && !isPro,
Expand Down Expand Up @@ -218,6 +220,7 @@ function BillingButton() {
plan: "pro",
});
await auth?.refreshSession();
openTrialBeginModal();
},
});

Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/components/ui/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function Modal({
<>
{showOverlay && (
<div
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
className="fixed inset-0 z-[100] bg-black/50 backdrop-blur-sm"
aria-hidden="true"
onClick={preventClose ? undefined : onClose}
>
Expand All @@ -69,7 +69,7 @@ export function Modal({
role="dialog"
aria-modal="true"
className={cn([
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
"fixed left-1/2 top-1/2 z-[100] -translate-x-1/2 -translate-y-1/2",
"overflow-clip rounded-lg bg-background shadow-lg",
sizeClasses[size],
className,
Expand Down
Loading