diff --git a/apps/desktop/src/routeTree.gen.ts b/apps/desktop/src/routeTree.gen.ts index e7ea0247ef..081a67f629 100644 --- a/apps/desktop/src/routeTree.gen.ts +++ b/apps/desktop/src/routeTree.gen.ts @@ -11,6 +11,7 @@ import { createFileRoute } from '@tanstack/react-router' import { Route as rootRouteImport } from './routes/__root' +import { Route as NotificationRouteImport } from './routes/notification' import { Route as AppRouteRouteImport } from './routes/app/route' import { Route as AppDevtoolRouteImport } from './routes/app/devtool' import { Route as AppOnboardingIndexRouteImport } from './routes/app/onboarding/index' @@ -22,6 +23,11 @@ import { Route as AppMainLayoutIndexRouteImport } from './routes/app/main/_layou const AppSettingsRouteImport = createFileRoute('/app/settings')() const AppMainRouteImport = createFileRoute('/app/main')() +const NotificationRoute = NotificationRouteImport.update({ + id: '/notification', + path: '/notification', + getParentRoute: () => rootRouteImport, +} as any) const AppRouteRoute = AppRouteRouteImport.update({ id: '/app', path: '/app', @@ -68,6 +74,7 @@ const AppMainLayoutIndexRoute = AppMainLayoutIndexRouteImport.update({ export interface FileRoutesByFullPath { '/app': typeof AppRouteRouteWithChildren + '/notification': typeof NotificationRoute '/app/devtool': typeof AppDevtoolRoute '/app/main': typeof AppMainLayoutRouteWithChildren '/app/settings': typeof AppSettingsLayoutRouteWithChildren @@ -77,6 +84,7 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/app': typeof AppRouteRouteWithChildren + '/notification': typeof NotificationRoute '/app/devtool': typeof AppDevtoolRoute '/app/main': typeof AppMainLayoutIndexRoute '/app/settings': typeof AppSettingsLayoutIndexRoute @@ -85,6 +93,7 @@ export interface FileRoutesByTo { export interface FileRoutesById { __root__: typeof rootRouteImport '/app': typeof AppRouteRouteWithChildren + '/notification': typeof NotificationRoute '/app/devtool': typeof AppDevtoolRoute '/app/main': typeof AppMainRouteWithChildren '/app/main/_layout': typeof AppMainLayoutRouteWithChildren @@ -98,6 +107,7 @@ export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath fullPaths: | '/app' + | '/notification' | '/app/devtool' | '/app/main' | '/app/settings' @@ -107,6 +117,7 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/app' + | '/notification' | '/app/devtool' | '/app/main' | '/app/settings' @@ -114,6 +125,7 @@ export interface FileRouteTypes { id: | '__root__' | '/app' + | '/notification' | '/app/devtool' | '/app/main' | '/app/main/_layout' @@ -126,10 +138,18 @@ export interface FileRouteTypes { } export interface RootRouteChildren { AppRouteRoute: typeof AppRouteRouteWithChildren + NotificationRoute: typeof NotificationRoute } declare module '@tanstack/react-router' { interface FileRoutesByPath { + '/notification': { + id: '/notification' + path: '/notification' + fullPath: '/notification' + preLoaderRoute: typeof NotificationRouteImport + parentRoute: typeof rootRouteImport + } '/app': { id: '/app' path: '/app' @@ -262,6 +282,7 @@ const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren( const rootRouteChildren: RootRouteChildren = { AppRouteRoute: AppRouteRouteWithChildren, + NotificationRoute: NotificationRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/desktop/src/routes/__root.tsx b/apps/desktop/src/routes/__root.tsx index 5cf803ab6c..79dafa57e0 100644 --- a/apps/desktop/src/routes/__root.tsx +++ b/apps/desktop/src/routes/__root.tsx @@ -1,11 +1,13 @@ import { createRootRouteWithContext, + type LinkProps, Outlet, useNavigate, } from "@tanstack/react-router"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; import { lazy, useEffect } from "react"; +import type { DeepLink } from "@hypr/plugin-deeplink2"; import { events as windowsEvents } from "@hypr/plugin-windows"; import { AuthProvider } from "../auth"; @@ -13,6 +15,10 @@ import { BillingProvider } from "../billing"; import { ErrorComponent, NotFoundComponent } from "../components/control"; import type { Context } from "../types"; +0 as DeepLink["to"] extends NonNullable + ? 0 + : "DeepLink['to'] must match a valid route"; + export const Route = createRootRouteWithContext>()({ component: Component, errorComponent: ErrorComponent, diff --git a/apps/desktop/src/routes/notification.tsx b/apps/desktop/src/routes/notification.tsx new file mode 100644 index 0000000000..be137a4eae --- /dev/null +++ b/apps/desktop/src/routes/notification.tsx @@ -0,0 +1,7 @@ +import { createFileRoute, redirect } from "@tanstack/react-router"; + +export const Route = createFileRoute("/notification")({ + beforeLoad: async () => { + throw redirect({ to: "/app/main" }); + }, +}); diff --git a/plugins/deeplink2/build.rs b/plugins/deeplink2/build.rs index 3ba86a40bb..a220cd4b25 100644 --- a/plugins/deeplink2/build.rs +++ b/plugins/deeplink2/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["ping", "get_available_deep_links"]; +const COMMANDS: &[&str] = &[]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/plugins/deeplink2/js/bindings.gen.ts b/plugins/deeplink2/js/bindings.gen.ts index 1189d7b7b4..0d10db462f 100644 --- a/plugins/deeplink2/js/bindings.gen.ts +++ b/plugins/deeplink2/js/bindings.gen.ts @@ -7,22 +7,7 @@ export const commands = { -async ping() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:deeplink2|ping") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async getAvailableDeepLinks() : Promise> { - try { - return { status: "ok", data: await TAURI_INVOKE("plugin:deeplink2|get_available_deep_links") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -} + } /** user-defined events **/ @@ -40,10 +25,10 @@ deepLinkEvent: "plugin:deeplink2:deep-link-event" /** user-defined types **/ -export type AuthDeepLink = { action: "Callback"; access_token: string; refresh_token: string } -export type DeepLink = { type: "Auth"; data: AuthDeepLink } | { type: "Unknown"; data: { url: string } } +export type DeepLink = { to: "/app/onboarding"; search: OnboardingSearch } export type DeepLinkEvent = DeepLink -export type DeepLinkInfo = { name: string; description: string; example: string } +export type OnboardingSearch = { step: OnboardingStep; local?: boolean } +export type OnboardingStep = "welcome" | "permissions" /** tauri-specta globals **/ diff --git a/plugins/deeplink2/src/commands.rs b/plugins/deeplink2/src/commands.rs deleted file mode 100644 index 9ff0c2e033..0000000000 --- a/plugins/deeplink2/src/commands.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::DeepLink2PluginExt; - -#[tauri::command] -#[specta::specta] -pub(crate) async fn ping(_app: tauri::AppHandle) -> Result { - Ok("pong".to_string()) -} - -#[tauri::command] -#[specta::specta] -pub(crate) async fn get_available_deep_links( - app: tauri::AppHandle, -) -> Result, String> { - Ok(app.get_available_deep_links()) -} diff --git a/plugins/deeplink2/src/error.rs b/plugins/deeplink2/src/error.rs index b7039b1ab1..c8e858c919 100644 --- a/plugins/deeplink2/src/error.rs +++ b/plugins/deeplink2/src/error.rs @@ -10,6 +10,8 @@ pub enum Error { UnknownPath(String), #[error("url parse error: {0}")] UrlParse(#[from] url::ParseError), + #[error("missing query parameter: {0}")] + MissingQueryParam(String), } impl Serialize for Error { diff --git a/plugins/deeplink2/src/ext.rs b/plugins/deeplink2/src/ext.rs deleted file mode 100644 index ed264026e2..0000000000 --- a/plugins/deeplink2/src/ext.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::{DeepLink, DeepLinkInfo}; - -pub trait DeepLink2PluginExt { - fn parse_deep_link(&self, url: &str) -> crate::Result; - fn get_available_deep_links(&self) -> Vec; -} - -impl> DeepLink2PluginExt for T { - fn parse_deep_link(&self, url: &str) -> crate::Result { - DeepLink::parse(url) - } - - fn get_available_deep_links(&self) -> Vec { - DeepLink::available_deep_links() - } -} diff --git a/plugins/deeplink2/src/lib.rs b/plugins/deeplink2/src/lib.rs index 321223b6df..003e6ad9cc 100644 --- a/plugins/deeplink2/src/lib.rs +++ b/plugins/deeplink2/src/lib.rs @@ -1,22 +1,16 @@ -mod commands; mod error; -mod ext; mod types; pub use error::{Error, Result}; -pub use ext::*; -pub use types::*; const PLUGIN_NAME: &str = "deeplink2"; fn make_specta_builder() -> tauri_specta::Builder { tauri_specta::Builder::::new() .plugin_name(PLUGIN_NAME) - .commands(tauri_specta::collect_commands![ - commands::ping::, - commands::get_available_deep_links::, - ]) - .events(tauri_specta::collect_events![events::DeepLinkEvent]) + .commands(tauri_specta::collect_commands![]) + .events(tauri_specta::collect_events![types::DeepLinkEvent]) + .typ::() .error_handling(tauri_specta::ErrorHandlingMode::Result) } @@ -29,11 +23,6 @@ pub fn init() -> tauri::plugin::TauriPlugin { .build() } -pub mod events { - #[derive(Debug, Clone, serde::Serialize, specta::Type, tauri_specta::Event)] - pub struct DeepLinkEvent(pub crate::DeepLink); -} - #[cfg(test)] mod test { use super::*; diff --git a/plugins/deeplink2/src/types.rs b/plugins/deeplink2/src/types.rs deleted file mode 100644 index 7d2f05bd05..0000000000 --- a/plugins/deeplink2/src/types.rs +++ /dev/null @@ -1,107 +0,0 @@ -use serde::{Deserialize, Serialize}; -use specta::Type; -use std::collections::HashMap; - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -pub struct DeepLinkInfo { - pub name: String, - pub description: String, - pub example: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(tag = "type", content = "data")] -pub enum DeepLink { - Auth(AuthDeepLink), - Unknown { url: String }, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Type)] -#[serde(tag = "action")] -pub enum AuthDeepLink { - Callback { - access_token: String, - refresh_token: String, - }, -} - -impl DeepLink { - pub fn parse(url: &str) -> crate::Result { - let parsed = url::Url::parse(url)?; - - let host = parsed.host_str().unwrap_or(""); - let path = parsed.path().trim_start_matches('/'); - let full_path = if path.is_empty() { - host.to_string() - } else { - format!("{}/{}", host, path) - }; - - let query_params: HashMap = parsed.query_pairs().into_owned().collect(); - - match full_path.as_str() { - "auth/callback" | "auth" => { - let access_token = query_params - .get("access_token") - .cloned() - .unwrap_or_default(); - let refresh_token = query_params - .get("refresh_token") - .cloned() - .unwrap_or_default(); - - Ok(DeepLink::Auth(AuthDeepLink::Callback { - access_token, - refresh_token, - })) - } - _ => Ok(DeepLink::Unknown { - url: url.to_string(), - }), - } - } - - pub fn available_deep_links() -> Vec { - vec![DeepLinkInfo { - name: "Auth Callback".to_string(), - description: "Handle authentication callback with access and refresh tokens" - .to_string(), - example: "hyprnote://auth/callback?access_token=xxx&refresh_token=yyy".to_string(), - }] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_auth_callback() { - let url = "hyprnote://auth/callback?access_token=test_access&refresh_token=test_refresh"; - let result = DeepLink::parse(url).unwrap(); - - match result { - DeepLink::Auth(AuthDeepLink::Callback { - access_token, - refresh_token, - }) => { - assert_eq!(access_token, "test_access"); - assert_eq!(refresh_token, "test_refresh"); - } - _ => panic!("Expected Auth::Callback"), - } - } - - #[test] - fn test_parse_unknown() { - let url = "hyprnote://unknown/path"; - let result = DeepLink::parse(url).unwrap(); - - match result { - DeepLink::Unknown { url: parsed_url } => { - assert_eq!(parsed_url, url); - } - _ => panic!("Expected Unknown"), - } - } -} diff --git a/plugins/deeplink2/src/types/mod.rs b/plugins/deeplink2/src/types/mod.rs new file mode 100644 index 0000000000..dbd9263757 --- /dev/null +++ b/plugins/deeplink2/src/types/mod.rs @@ -0,0 +1,50 @@ +mod notification; +pub use notification::*; + +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::collections::HashMap; +use std::str::FromStr; + +use crate::types::NotificationSearch; + +#[derive(Debug, Clone, serde::Serialize, specta::Type, tauri_specta::Event)] +pub struct DeepLinkEvent(pub DeepLink); + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +#[serde(tag = "to", content = "search")] +pub enum DeepLink { + #[serde(rename = "/notification")] + Notification(NotificationSearch), +} + +impl FromStr for DeepLink { + type Err = crate::Error; + + fn from_str(s: &str) -> Result { + let parsed = url::Url::parse(s)?; + + let host = parsed.host_str().unwrap_or(""); + let path = parsed.path().trim_start_matches('/'); + let full_path = if path.is_empty() { + host.to_string() + } else { + format!("{}/{}", host, path) + }; + + let query_params: HashMap = parsed.query_pairs().into_owned().collect(); + + match full_path.as_str() { + "notification" => { + let key = query_params + .get("key") + .ok_or(crate::Error::MissingQueryParam("key".to_string()))?; + + Ok(DeepLink::Notification(NotificationSearch { + key: key.to_string(), + })) + } + _ => Err(crate::Error::UnknownPath(full_path)), + } + } +} diff --git a/plugins/deeplink2/src/types/notification.rs b/plugins/deeplink2/src/types/notification.rs new file mode 100644 index 0000000000..69f4dedb93 --- /dev/null +++ b/plugins/deeplink2/src/types/notification.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct NotificationSearch { + pub key: String, +}