Skip to content
Closed
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
655 changes: 384 additions & 271 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
"remixicon": "^4.6.0",
"sonner": "^1.7.4",
"tailwind-merge": "^2.6.0",
"tauri-plugin-keygen-api": "github:bagindo/tauri-plugin-keygen#v2",
"tauri-plugin-keygen-rs2-api": "^0.2.3",
"tauri-plugin-sentry-api": "^0.4.1",
"tippy.js": "^6.3.7",
"zod": "^3.25.46",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ tauri-plugin-dialog = { workspace = true }
tauri-plugin-fs = { workspace = true, features = ["watch"] }
tauri-plugin-global-shortcut = "2"
tauri-plugin-http = { workspace = true }
tauri-plugin-keygen = { git = "https://github.com/bagindo/tauri-plugin-keygen", branch = "v2" }
tauri-plugin-os = "2"
tauri-plugin-prevent-default = { version = "1.2", features = ["unstable-windows"] }
tauri-plugin-process = "2"
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
{ "url": "http://**" }
]
},
"dialog:allow-save"
"dialog:allow-save",
"keygen:default"
]
}
44 changes: 44 additions & 0 deletions apps/desktop/src-tauri/src/deeplink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ pub fn parse(url: String) -> Vec<Destination> {
};

let dests = match parsed_url.path() {
// Specified in notification related codebase
"/notification" => parse_notification_query(&parsed_url),
// Specified in /apps/admin
"/register" => parse_register_query(&parsed_url),
// Specified in email template
"/license" => parse_license_query(&parsed_url),
_ => vec![Destination::default()],
};

Expand Down Expand Up @@ -80,6 +84,29 @@ fn parse_register_query(parsed_url: &url::Url) -> Vec<Destination> {
]
}

fn parse_license_query(parsed_url: &url::Url) -> Vec<Destination> {
let main_url = "/app".to_string();

let settings_url = match parsed_url.query() {
Some(query) => match serde_qs::from_str::<LicenseQuery>(query) {
Ok(params) => format!("/app/settings?tab=billing&key={}", params.key),
Err(_) => "/app/settings".to_string(),
},
None => "/app/settings".to_string(),
};

vec![
Destination {
window: HyprWindow::Main,
url: main_url,
},
Destination {
window: HyprWindow::Settings,
url: settings_url,
},
]
}

#[derive(serde::Serialize, serde::Deserialize)]
struct NotificationQuery {
event_id: Option<String>,
Expand All @@ -91,6 +118,11 @@ struct RegisterQuery {
api_key: String,
}

#[derive(serde::Serialize, serde::Deserialize)]
struct LicenseQuery {
key: String,
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -109,4 +141,16 @@ mod tests {
"/app/register?base_url=http://localhost:3000&api_key=123"
);
}

#[test]
fn test_parse_license_query() {
let url = "hypr://hyprnote.com/license?key=123";

let dests = parse(url.to_string());
assert_eq!(dests.len(), 1);

let dest = dests.first().unwrap();
assert_eq!(dest.window, HyprWindow::Main);
assert_eq!(dest.url, "/app");
}
}
7 changes: 7 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ pub async fn main() {
.plugin(tauri_plugin_membership::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(
tauri_plugin_keygen::Builder::new(
"76dfe152-397c-4689-9c5e-3669cefa34b9",
"13f18c98b8c1e5539d92df4aad2d51f4d203d5aead296215df7c3d6376b78b13",
)
.build(),
)
Comment on lines +102 to +108
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move product & hash out of source to env/CFG

Hard-coding the product UUID and checksum exposes them to the public repo history and requires a new build for every value change. Reading them from tauri.conf.json or env variables keeps secrets out of VCS and lets you rotate keys without shipping a new binary.

-        .plugin(
-            tauri_plugin_keygen::Builder::new(
-                "76dfe152-397c-4689-9c5e-3669cefa34b9",
-                "13f18c98b8c1e5539d92df4aad2d51f4d203d5aead296215df7c3d6376b78b13",
-            )
-            .build(),
-        )
+        .plugin(
+            tauri_plugin_keygen::Builder::new(
+                env!("KEYGEN_PRODUCT_ID"),
+                env!("KEYGEN_HASH"),
+            )
+            .build(),
+        )

(Adjust to your preferred config mechanism.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.plugin(
tauri_plugin_keygen::Builder::new(
"76dfe152-397c-4689-9c5e-3669cefa34b9",
"13f18c98b8c1e5539d92df4aad2d51f4d203d5aead296215df7c3d6376b78b13",
)
.build(),
)
.plugin(
tauri_plugin_keygen::Builder::new(
env!("KEYGEN_PRODUCT_ID"),
env!("KEYGEN_HASH"),
)
.build(),
)
🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/lib.rs around lines 102 to 108, the product UUID
and hash are hard-coded, exposing secrets in the source code and requiring
rebuilds for changes. Refactor the code to read these values from environment
variables or the tauri.conf.json configuration file instead. This will keep
secrets out of version control and allow key rotation without rebuilding the
binary. Adjust the code to load and pass these values dynamically to the
tauri_plugin_keygen::Builder.

.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec![]),
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/components/human-profile/past-notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function PastNotes({ human }: { human: Human }) {
} else {
const params = { to: "/app/new" } as const satisfies LinkProps;

windowsCommands.windowEmitNavigate({ type: "main" }, params.to).then(() => {
windowsCommands.windowEmitNavigate({ type: "main" }, { path: params.to, search: {} }).then(() => {
windowsCommands.windowDestroy({ type: "human", value: human.id });
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Trans } from "@lingui/react/macro";
import type { LinkProps } from "@tanstack/react-router";
import { getName, getVersion } from "@tauri-apps/api/app";
import { CogIcon, CpuIcon } from "lucide-react";
import { CastleIcon, CogIcon, ShieldIcon } from "lucide-react";
import { useState } from "react";

import Shortcut from "@/components/shortcut";
import { useHypr } from "@/contexts";
import { useLicense } from "@/hooks/use-license";
import { openURL } from "@/utils/shell";
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Button } from "@hypr/ui/components/ui/button";
Expand All @@ -21,6 +23,9 @@ export function SettingsButton() {
const [open, setOpen] = useState(false);
const { userId } = useHypr();

const { getLicense } = useLicense();
const isPro = !!getLicense.data?.valid;

const versionQuery = useQuery({
queryKey: ["appVersion"],
queryFn: async () => {
Expand All @@ -41,7 +46,13 @@ export function SettingsButton() {

const handleClickPlans = () => {
setOpen(false);
windowsCommands.windowShow({ type: "plans" });

windowsCommands.windowShow({ type: "settings" }).then(() => {
const params = { to: "/app/settings", search: { tab: "billing" } } as const satisfies LinkProps;
setTimeout(() => {
windowsCommands.windowEmitNavigate({ type: "settings" }, { path: params.to, search: params.search });
}, 500);
});
};

const handleClickChangelog = async () => {
Expand Down Expand Up @@ -71,27 +82,7 @@ export function SettingsButton() {
</DropdownMenuTrigger>

<DropdownMenuContent align="start" className="w-52 p-0">
<div
className={cn([
"px-2 py-3 bg-gradient-to-r rounded-t-md relative overflow-hidden cursor-pointer",
"from-gray-800 to-gray-900 hover:from-gray-700 hover:to-gray-800",
])}
onClick={handleClickPlans}
>
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZGVmcz48cGF0dGVybiBpZD0iZ3JpZCIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIiBwYXR0ZXJuVW5pdHM9InVzZXJTcGFjZU9uVXNlIj48cGF0aCBkPSJNIDIwIDAgTCAwIDAgTCAwIDIwIiBmaWxsPSJub25lIiBzdHJva2U9InJnYmEoMjU1LDI1NSwyNTUsMC4xNSkiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2dyaWQpIi8+PC9zdmc+')] opacity-70">
</div>
<div className="flex items-center gap-3 text-white relative z-10">
<CpuIcon className="size-8 animate-pulse" />
<div>
<div className="font-medium">
<Trans>Local mode</Trans>
</div>
<div className="text-xs text-white/80 mt-0.5">
Privacy-focused AI
</div>
</div>
</div>
</div>
<DropdownHeader handleClick={handleClickPlans} isPro={isPro} />

<div className="p-1">
<DropdownMenuItem
Expand Down Expand Up @@ -124,3 +115,37 @@ export function SettingsButton() {
</DropdownMenu>
);
}

function DropdownHeader({
isPro,
handleClick,
}: {
isPro: boolean;
handleClick: () => void;
}) {
return (
<div
onClick={handleClick}
className={cn([
"px-3 py-2 bg-gradient-to-r rounded-t-md relative overflow-hidden cursor-pointer",
isPro
? "from-blue-700 to-blue-800 hover:from-blue-600 hover:to-blue-700"
: "from-gray-800 to-gray-900 hover:from-gray-700 hover:to-gray-800",
])}
>
<div className="absolute inset-0 opacity-70">
</div>
<div className="flex items-center gap-3 text-white relative z-10">
{isPro ? <CastleIcon className="size-8 animate-pulse" /> : <ShieldIcon className="size-8 animate-pulse" />}
<div>
<div className="font-medium">
{isPro ? "Pro Plan" : "Free Plan"}
</div>
<div className="text-xs text-white/80 mt-0.5">
{isPro ? "Full features" : "Basic features"}
</div>
</div>
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions apps/desktop/src/components/license.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect } from "react";

import { useLicense } from "@/hooks/use-license";

export function LicenseRefreshProvider({ children }: { children: React.ReactNode }) {
const { shouldRefresh, refreshLicense, getLicense } = useLicense();

useEffect(() => {
if (getLicense.isLoading) {
return;
}

const checkAndRefresh = () => {
if (shouldRefresh() && !refreshLicense.isPending) {
refreshLicense.mutate({});
}
};

checkAndRefresh();
const interval = setInterval(checkAndRefresh, 1000 * 60 * 10); // 10min

return () => clearInterval(interval);
}, [shouldRefresh, refreshLicense, getLicense.isLoading, refreshLicense.isPending]);

return <>{children}</>;
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Badge } from "@hypr/ui/components/ui/badge";
import { Trans } from "@lingui/react/macro";
import type { LinkProps } from "@tanstack/react-router";
import { memo, useCallback } from "react";

import { commands as windowsCommands } from "@hypr/plugin-windows";
import { Badge } from "@hypr/ui/components/ui/badge";

interface EmptyChatStateProps {
onQuickAction: (prompt: string) => void;
onFocusInput: () => void;
Expand All @@ -20,7 +22,11 @@ export const EmptyChatState = memo(({ onQuickAction, onFocusInput }: EmptyChatSt
const handleCustomEndpointsClick = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
windowsCommands.windowShow({ type: "settings" }).then(() => {
windowsCommands.windowNavigate({ type: "settings" }, "/app/settings?tab=ai");
const params = { to: "/app/settings", search: { tab: "ai" } } as const satisfies LinkProps;

setTimeout(() => {
windowsCommands.windowEmitNavigate({ type: "settings" }, { path: params.to, search: params.search });
}, 500);
});
}, []);

Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/components/settings/components/tab-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
BellIcon,
BlocksIcon,
CalendarIcon,
FlaskConicalIcon,
CreditCardIcon,
LayoutTemplateIcon,
MessageSquareIcon,
SettingsIcon,
Expand All @@ -20,8 +20,6 @@ export function TabIcon({ tab }: { tab: Tab }) {
return <BellIcon className="h-4 w-4" />;
case "sound":
return <AudioLinesIcon className="h-4 w-4" />;
case "lab":
return <FlaskConicalIcon className="h-4 w-4" />;
case "feedback":
return <MessageSquareIcon className="h-4 w-4" />;
case "ai":
Expand All @@ -32,6 +30,8 @@ export function TabIcon({ tab }: { tab: Tab }) {
return <LayoutTemplateIcon className="h-4 w-4" />;
case "integrations":
return <BlocksIcon className="h-4 w-4" />;
case "billing":
return <CreditCardIcon className="h-4 w-4" />;
default:
return null;
}
Expand Down
20 changes: 15 additions & 5 deletions apps/desktop/src/components/settings/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { LucideIcon } from "lucide-react";
import { Bell, Calendar, LayoutTemplate, MessageSquare, Settings, Sparkles, Volume2 } from "lucide-react";
import {
Bell,
BlocksIcon,
Calendar,
CreditCard,
LayoutTemplate,
MessageSquare,
Settings,
Sparkles,
Volume2,
} from "lucide-react";

export type Tab =
| "general"
Expand All @@ -8,9 +18,9 @@ export type Tab =
| "notifications"
| "sound"
| "templates"
| "lab"
| "feedback"
| "integrations";
| "integrations"
| "billing";

export const TABS: { name: Tab; icon: LucideIcon }[] = [
{ name: "general", icon: Settings },
Expand All @@ -19,7 +29,7 @@ export const TABS: { name: Tab; icon: LucideIcon }[] = [
{ name: "notifications", icon: Bell },
{ name: "sound", icon: Volume2 },
{ name: "templates", icon: LayoutTemplate },
// { name: "lab", icon: FlaskConical },
{ name: "integrations", icon: MessageSquare },
{ name: "feedback", icon: MessageSquare },
{ name: "feedback", icon: BlocksIcon },
{ name: "billing", icon: CreditCard },
];
Loading
Loading