From 87d6c3adb93239d36caa7c099ea86511389425bc Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 9 May 2025 14:28:42 -0700 Subject: [PATCH 01/26] wip --- Cargo.lock | 1 - apps/app/server/src/main.rs | 51 ++++++++--------- .../src-tauri/capabilities/default.json | 6 +- apps/desktop/src/components/login-modal.tsx | 40 +++++++++++-- .../src/components/settings/views/lab.tsx | 36 ------------ apps/desktop/src/contexts/index.ts | 1 - apps/desktop/src/contexts/login-modal.tsx | 57 ------------------- crates/db-user/src/events_types.rs | 2 +- plugins/auth/js/bindings.gen.ts | 2 +- plugins/auth/src/store.rs | 4 ++ plugins/connector/Cargo.toml | 4 +- plugins/connector/src/ext.rs | 14 +++-- 12 files changed, 79 insertions(+), 139 deletions(-) delete mode 100644 apps/desktop/src/contexts/login-modal.tsx diff --git a/Cargo.lock b/Cargo.lock index c7ffc2e33..3879e24f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12944,7 +12944,6 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-auth", - "tauri-plugin-flags", "tauri-plugin-local-llm", "tauri-plugin-local-stt", "tauri-plugin-store2", diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index d57cf5a46..be256831a 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -42,7 +42,7 @@ use clerk_rs::{ ClerkConfiguration, }; -use state::{AuthState, WorkerState}; +use state::{AnalyticsState, AuthState, WorkerState}; fn main() { #[cfg(debug_assertions)] @@ -182,17 +182,12 @@ fn main() { "/integration/connection", api_post(web::integration::create_connection), ) - .layer( - tower::builder::ServiceBuilder::new() - .layer(axum::middleware::from_fn_with_state( - AuthState::from_ref(&state), - middleware::attach_user_from_clerk, - )) - .layer(axum::middleware::from_fn_with_state( - AuthState::from_ref(&state), - middleware::attach_user_db, - )), - ); + .layer(tower::builder::ServiceBuilder::new().layer( + axum::middleware::from_fn_with_state( + AuthState::from_ref(&state), + middleware::attach_user_from_clerk, + ), + )); let web_router = web_connect_router .merge(web_other_router) @@ -208,22 +203,22 @@ fn main() { api_get(native::user::list_integrations), ) .api_route("/subscription", api_get(native::subscription::handler)) - .route("/listen/realtime", get(native::listen::realtime::handler)); - // .layer( - // tower::builder::ServiceBuilder::new() - // .layer(axum::middleware::from_fn_with_state( - // AuthState::from_ref(&state), - // middleware::verify_api_key, - // )) - // .layer(axum::middleware::from_fn_with_state( - // AuthState::from_ref(&state), - // middleware::attach_user_db, - // )) - // .layer(axum::middleware::from_fn_with_state( - // AnalyticsState::from_ref(&state), - // middleware::send_analytics, - // )), - // ); + .route("/listen/realtime", get(native::listen::realtime::handler)) + .layer( + tower::builder::ServiceBuilder::new() + .layer(axum::middleware::from_fn_with_state( + AuthState::from_ref(&state), + middleware::verify_api_key, + )) + .layer(axum::middleware::from_fn_with_state( + AuthState::from_ref(&state), + middleware::attach_user_db, + )) + .layer(axum::middleware::from_fn_with_state( + AnalyticsState::from_ref(&state), + middleware::send_analytics, + )), + ); let slack_router = ApiRouter::new(); diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index a0a1997cc..7deca765a 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -50,7 +50,11 @@ "fs:default", { "identifier": "opener:allow-open-url", - "allow": [{ "url": "https://**" }] + "allow": [ + { "url": "https://**" }, + { "url": "http://localhost:*" }, + { "url": "http://127.0.0.1:*" } + ] }, { "identifier": "fs:allow-exists", diff --git a/apps/desktop/src/components/login-modal.tsx b/apps/desktop/src/components/login-modal.tsx index a401f84e5..22c78b3d1 100644 --- a/apps/desktop/src/components/login-modal.tsx +++ b/apps/desktop/src/components/login-modal.tsx @@ -1,8 +1,11 @@ import { Trans, useLingui } from "@lingui/react/macro"; +import { useQuery } from "@tanstack/react-query"; import { useNavigate } from "@tanstack/react-router"; import { message } from "@tauri-apps/plugin-dialog"; +import { openUrl } from "@tauri-apps/plugin-opener"; import { useEffect, useState } from "react"; +import { baseUrl } from "@/client"; import { commands } from "@/types"; import { commands as authCommands, events } from "@hypr/plugin-auth"; import { commands as sfxCommands } from "@hypr/plugin-sfx"; @@ -10,6 +13,7 @@ import { Modal, ModalBody } from "@hypr/ui/components/ui/modal"; import { Particles } from "@hypr/ui/components/ui/particles"; import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { TextAnimate } from "@hypr/ui/components/ui/text-animate"; +import { cn } from "@hypr/ui/lib/utils"; interface LoginModalProps { isOpen: boolean; @@ -33,9 +37,7 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { events.authEvent .listen(({ payload }) => { if (payload === "success") { - commands.setupDbForCloud().then(() => { - onClose(); - }); + message("Successfully authenticated!"); return; } @@ -67,6 +69,25 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { } }, [isOpen]); + const url = useQuery({ + queryKey: ["oauth-url", port], + enabled: !!port, + queryFn: () => { + const u = new URL(baseUrl); + u.pathname = "/auth/connect"; + u.searchParams.set("c", window.crypto.randomUUID()); + u.searchParams.set("f", "fingerprint"); + u.searchParams.set("p", port!.toString()); + return u.toString(); + }, + }); + + const handleStartCloud = () => { + if (url.data) { + openUrl(url.data); + } + }; + const handleStartLocal = () => { onClose(); }; @@ -99,11 +120,22 @@ export function LoginModal({ isOpen, onClose }: LoginModalProps) { Get Started + + diff --git a/apps/desktop/src/components/settings/views/lab.tsx b/apps/desktop/src/components/settings/views/lab.tsx index 9747f7341..dd9202d04 100644 --- a/apps/desktop/src/components/settings/views/lab.tsx +++ b/apps/desktop/src/components/settings/views/lab.tsx @@ -1,6 +1,5 @@ import { Trans } from "@lingui/react/macro"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { CloudLightningIcon } from "lucide-react"; import { useEffect, useState } from "react"; import { commands as flagsCommands } from "@hypr/plugin-flags"; @@ -11,46 +10,11 @@ export default function Lab() {
-
); } -function CloudPreview() { - const flagQuery = useQuery({ - queryKey: ["flags", "CloudPreview"], - queryFn: () => flagsCommands.isEnabled("CloudPreview"), - }); - - const flagMutation = useMutation({ - mutationFn: async (enabled: boolean) => { - if (enabled) { - flagsCommands.enable("CloudPreview"); - } else { - flagsCommands.disable("CloudPreview"); - } - }, - onSuccess: () => { - flagQuery.refetch(); - }, - }); - - const handleToggle = (enabled: boolean) => { - flagMutation.mutate(enabled); - }; - - return ( - } - enabled={flagQuery.data ?? false} - onToggle={handleToggle} - /> - ); -} - function ChatPanel() { const flagQuery = useQuery({ queryKey: ["flags", "ChatRightPanel"], diff --git a/apps/desktop/src/contexts/index.ts b/apps/desktop/src/contexts/index.ts index 2fc8ad79a..6150051cb 100644 --- a/apps/desktop/src/contexts/index.ts +++ b/apps/desktop/src/contexts/index.ts @@ -1,7 +1,6 @@ export * from "./edit-mode-context"; export * from "./hypr"; export * from "./left-sidebar"; -export * from "./login-modal"; export * from "./new-note"; export * from "./right-panel"; export * from "./search"; diff --git a/apps/desktop/src/contexts/login-modal.tsx b/apps/desktop/src/contexts/login-modal.tsx deleted file mode 100644 index e5b22f35c..000000000 --- a/apps/desktop/src/contexts/login-modal.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from "react"; - -interface LoginModalContextType { - isLoginModalOpen: boolean; - openLoginModal: () => void; - closeLoginModal: () => void; - shouldShowLoginModal: boolean; - setShouldShowLoginModal: (value: boolean) => void; -} - -const LoginModalContext = createContext(undefined); - -export function useLoginModal() { - const context = useContext(LoginModalContext); - if (context === undefined) { - throw new Error("useLoginModal must be used within a LoginModalProvider"); - } - return context; -} - -interface LoginModalProviderProps { - children: React.ReactNode; -} - -export function LoginModalProvider({ children }: LoginModalProviderProps) { - const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); - const [shouldShowLoginModal, setShouldShowLoginModal] = useState(true); - - useEffect(() => { - const hasLoginBeenDismissed = localStorage.getItem("loginModalDismissed") === "true"; - - if (shouldShowLoginModal && !hasLoginBeenDismissed) { - setIsLoginModalOpen(true); - } - }, [shouldShowLoginModal]); - - const openLoginModal = () => setIsLoginModalOpen(true); - - const closeLoginModal = () => { - localStorage.setItem("loginModalDismissed", "true"); - setIsLoginModalOpen(false); - }; - - return ( - - {children} - - ); -} diff --git a/crates/db-user/src/events_types.rs b/crates/db-user/src/events_types.rs index d5ba1975b..af772279e 100644 --- a/crates/db-user/src/events_types.rs +++ b/crates/db-user/src/events_types.rs @@ -40,6 +40,6 @@ user_common_derives! { #[serde(rename = "search")] Search { query: String }, #[serde(rename = "dateRange")] - DateRange { start: DateTime, end: DateTime }, + DateRange { start: DateTime, end: DateTime } } } diff --git a/plugins/auth/js/bindings.gen.ts b/plugins/auth/js/bindings.gen.ts index 5f12da3d1..866ee74e0 100644 --- a/plugins/auth/js/bindings.gen.ts +++ b/plugins/auth/js/bindings.gen.ts @@ -51,7 +51,7 @@ authEvent: "plugin:auth:auth-event" export type AuthEvent = "success" | { error: string } export type RequestParams = { c: string; f: string; p: number } export type ResponseParams = { ui: string; ai: string; st: string; dt: string } -export type StoreKey = "auth-user-id" | "auth-account-id" +export type StoreKey = "auth-user-id" | "auth-account-id" | "auth-plan" export type VaultKey = "remote-database" | "remote-server" | "twenty-api-key" /** tauri-specta globals **/ diff --git a/plugins/auth/src/store.rs b/plugins/auth/src/store.rs index 5dcadaf10..f37872b85 100644 --- a/plugins/auth/src/store.rs +++ b/plugins/auth/src/store.rs @@ -10,6 +10,10 @@ pub enum StoreKey { #[serde(rename = "auth-account-id")] #[specta(rename = "auth-account-id")] AccountId, + #[strum(serialize = "auth-plan")] + #[serde(rename = "auth-plan")] + #[specta(rename = "auth-plan")] + Plan, } pub fn get_store>( diff --git a/plugins/connector/Cargo.toml b/plugins/connector/Cargo.toml index 0d4235ed7..85be34068 100644 --- a/plugins/connector/Cargo.toml +++ b/plugins/connector/Cargo.toml @@ -14,12 +14,10 @@ tauri-plugin = { workspace = true, features = ["build"] } specta-typescript = { workspace = true } [dependencies] +tauri = { workspace = true, features = ["test"] } tauri-plugin-auth = { workspace = true } tauri-plugin-local-llm = { workspace = true } tauri-plugin-local-stt = { workspace = true } - -tauri = { workspace = true, features = ["test"] } -tauri-plugin-flags = { workspace = true } tauri-plugin-store2 = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } diff --git a/plugins/connector/src/ext.rs b/plugins/connector/src/ext.rs index bebe01da8..f91da9489 100644 --- a/plugins/connector/src/ext.rs +++ b/plugins/connector/src/ext.rs @@ -105,11 +105,12 @@ impl> ConnectorPluginExt for T { async fn get_llm_connection(&self) -> Result { { - use tauri_plugin_flags::{FlagsPluginExt, StoreKey as FlagsStoreKey}; + use tauri_plugin_auth::{AuthPluginExt, StoreKey}; if self - .is_enabled(FlagsStoreKey::CloudPreview) - .unwrap_or(false) + .get_from_store(StoreKey::Plan)? + .unwrap_or("free".to_string()) + == "pro".to_string() { let api_base = if cfg!(debug_assertions) { "http://127.0.0.1:1234".to_string() @@ -168,11 +169,12 @@ impl> ConnectorPluginExt for T { async fn get_stt_connection(&self) -> Result { { - use tauri_plugin_flags::{FlagsPluginExt, StoreKey as FlagsStoreKey}; + use tauri_plugin_auth::{AuthPluginExt, StoreKey}; if self - .is_enabled(FlagsStoreKey::CloudPreview) - .unwrap_or(false) + .get_from_store(StoreKey::Plan)? + .unwrap_or("free".to_string()) + == "pro".to_string() { let api_base = if cfg!(debug_assertions) { "http://127.0.0.1:1234".to_string() From 9905891f7f8ffe12a111058f9e8f21412e099312 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 9 May 2025 21:17:40 -0700 Subject: [PATCH 02/26] checkout wip --- apps/app/server/openapi.gen.json | 12 ++++ apps/app/server/src/main.rs | 1 + apps/app/server/src/web/checkout.rs | 60 +++++++++++++++++++ apps/app/server/src/web/mod.rs | 1 + crates/db-admin/src/billings_migration.sql | 4 +- crates/db-admin/src/billings_ops.rs | 28 ++++++++- crates/db-admin/src/billings_types.rs | 4 +- .../generated/@tanstack/react-query.gen.ts | 21 ++++++- packages/client/generated/sdk.gen.ts | 9 ++- packages/client/generated/types.gen.ts | 18 +++++- 10 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 apps/app/server/src/web/checkout.rs diff --git a/apps/app/server/openapi.gen.json b/apps/app/server/openapi.gen.json index 8528f38d4..9872dbe3d 100644 --- a/apps/app/server/openapi.gen.json +++ b/apps/app/server/openapi.gen.json @@ -69,6 +69,18 @@ } } }, + "/api/web/checkout": { + "get": { + "responses": { + "200": { + "description": "plain text", + "content": { + "text/plain; charset=utf-8": {} + } + } + } + } + }, "/api/web/session/{id}": { "get": { "parameters": [ diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index be256831a..34b8482e6 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -177,6 +177,7 @@ fn main() { ApiRouter::new().api_route("/connect", api_post(web::connect::handler)); let web_other_router = ApiRouter::new() + .api_route("/checkout", api_get(web::checkout::handler)) .api_route("/session/{id}", api_get(web::session::handler)) .api_route( "/integration/connection", diff --git a/apps/app/server/src/web/checkout.rs b/apps/app/server/src/web/checkout.rs new file mode 100644 index 000000000..f3cbfd1ab --- /dev/null +++ b/apps/app/server/src/web/checkout.rs @@ -0,0 +1,60 @@ +use axum::{ + extract::{Extension, State}, + http::StatusCode, +}; + +use clerk_rs::validators::authorizer::ClerkJwt; +use stripe::{ + CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems, + CreateCustomer, Customer, +}; + +use crate::state::AppState; + +const PRICE_ID: &str = "price_1PZ00000000000000000000"; + +pub async fn handler( + State(state): State, + Extension(jwt): Extension, +) -> Result { + let account = state + .admin_db + .get_account_by_clerk_user_id(&jwt.sub) + .await + .unwrap() + .unwrap(); + + let billing = state + .admin_db + .get_billing_by_account_id(&account.id) + .await + .unwrap(); + + let customer_id = match billing.and_then(|b| b.stripe_customer) { + Some(c) => c.id, + None => { + let c = Customer::create(&state.stripe, CreateCustomer::default()) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + c.id + } + }; + + let checkout_session = { + let mut params = CreateCheckoutSession::new(); + params.cancel_url = Some("http://test.com/cancel"); + params.success_url = Some("http://test.com/success"); + params.customer = Some(customer_id); + params.mode = Some(CheckoutSessionMode::Subscription); + params.line_items = Some(vec![CreateCheckoutSessionLineItems { + quantity: Some(1), + price: Some(PRICE_ID.to_string()), + ..Default::default() + }]); + + CheckoutSession::create(&state.stripe, params).await? + }; + + Ok(checkout_session.url.unwrap()) +} diff --git a/apps/app/server/src/web/mod.rs b/apps/app/server/src/web/mod.rs index 8e1846de7..0655a52c1 100644 --- a/apps/app/server/src/web/mod.rs +++ b/apps/app/server/src/web/mod.rs @@ -1,3 +1,4 @@ +pub mod checkout; pub mod connect; pub mod integration; pub mod session; diff --git a/crates/db-admin/src/billings_migration.sql b/crates/db-admin/src/billings_migration.sql index 183285f66..946b49a37 100644 --- a/crates/db-admin/src/billings_migration.sql +++ b/crates/db-admin/src/billings_migration.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS billings ( id TEXT PRIMARY KEY, - organization_id TEXT NOT NULL, + account_id TEXT NOT NULL, stripe_customer TEXT NOT NULL, stripe_subscription TEXT NOT NULL, - FOREIGN KEY (organization_id) REFERENCES organizations(id) + FOREIGN KEY (account_id) REFERENCES accounts(id) ); diff --git a/crates/db-admin/src/billings_ops.rs b/crates/db-admin/src/billings_ops.rs index 7771ac5a7..e294e0108 100644 --- a/crates/db-admin/src/billings_ops.rs +++ b/crates/db-admin/src/billings_ops.rs @@ -1,9 +1,31 @@ use super::{AdminDatabase, Billing}; impl AdminDatabase { + pub async fn get_billing_by_account_id( + &self, + account_id: impl Into, + ) -> Result, crate::Error> { + let conn = self.conn()?; + + let mut rows = conn + .query( + "SELECT * FROM billings WHERE account_id = ?", + vec![account_id.into()], + ) + .await?; + + match rows.next().await? { + None => Ok(None), + Some(row) => { + let billing: Billing = libsql::de::from_row(&row).unwrap(); + Ok(Some(billing)) + } + } + } + pub async fn create_empty_billing( &self, - organization_id: impl Into, + account_id: impl Into, ) -> Result, crate::Error> { let conn = self.conn()?; @@ -11,14 +33,14 @@ impl AdminDatabase { .query( "INSERT INTO billings ( id, - organization_id, + account_id, stripe_subscription, stripe_customer ) VALUES (?, ?, ?, ?) RETURNING *", vec![ libsql::Value::Text(uuid::Uuid::new_v4().to_string()), - libsql::Value::Text(organization_id.into()), + libsql::Value::Text(account_id.into()), libsql::Value::Null, libsql::Value::Null, ], diff --git a/crates/db-admin/src/billings_types.rs b/crates/db-admin/src/billings_types.rs index 86addb502..f13819762 100644 --- a/crates/db-admin/src/billings_types.rs +++ b/crates/db-admin/src/billings_types.rs @@ -3,7 +3,7 @@ use crate::admin_common_derives; admin_common_derives! { pub struct Billing { pub id: String, - pub organization_id: String, + pub account_id: String, #[schemars(skip)] pub stripe_subscription: Option, #[schemars(skip)] @@ -15,7 +15,7 @@ impl Billing { pub fn from_row(row: &libsql::Row) -> Result { Ok(Self { id: row.get(0).expect("id"), - organization_id: row.get(1).expect("organization_id"), + account_id: row.get(1).expect("account_id"), stripe_subscription: row .get_str(2) .map(|s| serde_json::from_str(s).unwrap()) diff --git a/packages/client/generated/@tanstack/react-query.gen.ts b/packages/client/generated/@tanstack/react-query.gen.ts index 524f352c0..5db4dcced 100644 --- a/packages/client/generated/@tanstack/react-query.gen.ts +++ b/packages/client/generated/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getHealth, getApiDesktopUserIntegrations, getApiDesktopSubscription, postApiWebConnect, getApiWebSessionById, postApiWebIntegrationConnection, postChatCompletions } from '../sdk.gen'; +import { type Options, getHealth, getApiDesktopUserIntegrations, getApiDesktopSubscription, postApiWebConnect, getApiWebCheckout, getApiWebSessionById, postApiWebIntegrationConnection, postChatCompletions } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError } from '@tanstack/react-query'; -import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopSubscriptionData, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebSessionByIdData, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from '../types.gen'; +import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopSubscriptionData, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from '../types.gen'; import { client as _heyApiClient } from '../client.gen'; export type QueryKey = [ @@ -118,6 +118,23 @@ export const postApiWebConnectMutation = (options?: Partial) => createQueryKey('getApiWebCheckout', options); + +export const getApiWebCheckoutOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getApiWebCheckout({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getApiWebCheckoutQueryKey(options) + }); +}; + export const getApiWebSessionByIdQueryKey = (options: Options) => createQueryKey('getApiWebSessionById', options); export const getApiWebSessionByIdOptions = (options: Options) => { diff --git a/packages/client/generated/sdk.gen.ts b/packages/client/generated/sdk.gen.ts index 7ab49dcb2..5ea8e8990 100644 --- a/packages/client/generated/sdk.gen.ts +++ b/packages/client/generated/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopUserIntegrationsResponse, GetApiDesktopSubscriptionData, GetApiDesktopSubscriptionResponse, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebSessionByIdData, GetApiWebSessionByIdResponse, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from './types.gen'; +import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopUserIntegrationsResponse, GetApiDesktopSubscriptionData, GetApiDesktopSubscriptionResponse, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, GetApiWebSessionByIdResponse, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -50,6 +50,13 @@ export const postApiWebConnect = (options: }); }; +export const getApiWebCheckout = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/api/web/checkout', + ...options + }); +}; + export const getApiWebSessionById = (options: Options) => { return (options.client ?? _heyApiClient).get({ url: '/api/web/session/{id}', diff --git a/packages/client/generated/types.gen.ts b/packages/client/generated/types.gen.ts index c75f74bd8..10cd65848 100644 --- a/packages/client/generated/types.gen.ts +++ b/packages/client/generated/types.gen.ts @@ -385,8 +385,9 @@ export type CreateChatCompletionRequest = { }; export type DiarizationChunk = { + confidence?: number | null; end: number; - speaker: string; + speaker: number; start: number; }; @@ -583,6 +584,7 @@ export type Subscription = { }; export type TranscriptChunk = { + confidence?: number | null; end: number; start: number; text: string; @@ -634,6 +636,20 @@ export type PostApiWebConnectResponses = { export type PostApiWebConnectResponse = PostApiWebConnectResponses[keyof PostApiWebConnectResponses]; +export type GetApiWebCheckoutData = { + body?: never; + path?: never; + query?: never; + url: '/api/web/checkout'; +}; + +export type GetApiWebCheckoutResponses = { + /** + * plain text + */ + 200: unknown; +}; + export type GetApiWebSessionByIdData = { body?: never; path: { From dca0efa9049e4aa89fa0c01c52e06de6042b3d99 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 9 May 2025 21:55:04 -0700 Subject: [PATCH 03/26] stripe wip --- apps/app/server/src/main.rs | 6 +- apps/app/server/src/stripe/mod.rs | 2 + apps/app/server/src/stripe/ops.rs | 106 ++++++++++++++++++ .../src/{stripe.rs => stripe/webhook.rs} | 0 apps/app/server/src/web/checkout.rs | 51 ++++----- apps/app/server/src/web/connect.rs | 12 +- 6 files changed, 142 insertions(+), 35 deletions(-) create mode 100644 apps/app/server/src/stripe/mod.rs create mode 100644 apps/app/server/src/stripe/ops.rs rename apps/app/server/src/{stripe.rs => stripe/webhook.rs} (100%) diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index 34b8482e6..b5a90a283 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -5,8 +5,8 @@ mod native; mod openapi; mod slack; mod state; -#[path = "stripe.rs"] -mod stripe_webhook; +#[path = "stripe/mod.rs"] +mod stripe_mod; mod types; mod web; mod worker; @@ -225,7 +225,7 @@ fn main() { let webhook_router = ApiRouter::new() .route("/nango", post(nango::handler)) - .route("/stripe", post(stripe_webhook::handler)) + .route("/stripe", post(stripe_mod::webhook::handler)) .nest("/slack", slack_router); let mut router = ApiRouter::new() diff --git a/apps/app/server/src/stripe/mod.rs b/apps/app/server/src/stripe/mod.rs new file mode 100644 index 000000000..516021e4c --- /dev/null +++ b/apps/app/server/src/stripe/mod.rs @@ -0,0 +1,2 @@ +pub mod ops; +pub mod webhook; diff --git a/apps/app/server/src/stripe/ops.rs b/apps/app/server/src/stripe/ops.rs new file mode 100644 index 000000000..15524c1d1 --- /dev/null +++ b/apps/app/server/src/stripe/ops.rs @@ -0,0 +1,106 @@ +use stripe::{ + CheckoutSession, CheckoutSessionMode, Client, CreateCheckoutSession, + CreateCheckoutSessionLineItems, CreateCheckoutSessionSubscriptionDataTrialSettings, + CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, + CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, + CreateSubscription, CreateSubscriptionTrialSettings, + CreateSubscriptionTrialSettingsEndBehavior, + CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, CustomerId, Subscription, +}; + +#[cfg(debug_assertions)] +pub fn get_price_id() -> String { + "price_1PZ00000000000000000000".to_string() +} + +#[cfg(not(debug_assertions))] +pub fn get_price_id() -> String { + "price_1PZ00000000000000000000".to_string() +} + +pub fn get_line_items() -> Vec { + vec![CreateCheckoutSessionLineItems { + quantity: Some(1), + price: Some(get_price_id()), + ..Default::default() + }] +} + +pub async fn create_checkout_without_trial( + client: &Client, + customer_id: CustomerId, +) -> Result { + let mut params = CreateCheckoutSession::new(); + params.customer = Some(customer_id); + params.mode = Some(CheckoutSessionMode::Subscription); + params.line_items = Some(get_line_items()); + + let cancel_url = get_cancel_url(); + let success_url = get_success_url(); + params.cancel_url = Some(&cancel_url); + params.success_url = Some(&success_url); + + CheckoutSession::create(client, params) + .await + .map_err(|e| e.to_string()) +} + +pub async fn create_subscription_with_trial( + client: &Client, + customer_id: CustomerId, +) -> Result { + let mut params = CreateSubscription::new(customer_id); + + params.trial_settings = Some(CreateSubscriptionTrialSettings { + end_behavior: CreateSubscriptionTrialSettingsEndBehavior { + missing_payment_method: + CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, + }, + }); + params.trial_period_days = Some(7); + + let subscription = Subscription::create(client, params) + .await + .map_err(|e| e.to_string())?; + + Ok(subscription) +} + +// https://docs.stripe.com/billing/subscriptions/trials#create-free-trials-without-payment +fn trial_settings_for_subscription() -> CreateSubscriptionTrialSettings { + CreateSubscriptionTrialSettings { + end_behavior: CreateSubscriptionTrialSettingsEndBehavior { + missing_payment_method: + CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, + }, + } +} + +#[allow(dead_code)] +fn trial_settings_for_checkout() -> CreateCheckoutSessionSubscriptionDataTrialSettings { + CreateCheckoutSessionSubscriptionDataTrialSettings { + end_behavior: CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior { + missing_payment_method: CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, + }, + } +} + +#[cfg(debug_assertions)] +fn get_cancel_url() -> String { + "http://test.com/cancel".to_string() +} + +#[cfg(not(debug_assertions))] +fn get_cancel_url() -> String { + "http://test.com/cancel".to_string() +} + +#[cfg(debug_assertions)] +fn get_success_url() -> String { + "http://test.com/success".to_string() +} + +#[cfg(not(debug_assertions))] +fn get_success_url() -> String { + "http://test.com/success".to_string() +} diff --git a/apps/app/server/src/stripe.rs b/apps/app/server/src/stripe/webhook.rs similarity index 100% rename from apps/app/server/src/stripe.rs rename to apps/app/server/src/stripe/webhook.rs diff --git a/apps/app/server/src/web/checkout.rs b/apps/app/server/src/web/checkout.rs index f3cbfd1ab..2c35245f3 100644 --- a/apps/app/server/src/web/checkout.rs +++ b/apps/app/server/src/web/checkout.rs @@ -4,31 +4,37 @@ use axum::{ }; use clerk_rs::validators::authorizer::ClerkJwt; -use stripe::{ - CheckoutSession, CheckoutSessionMode, CreateCheckoutSession, CreateCheckoutSessionLineItems, - CreateCustomer, Customer, -}; - -use crate::state::AppState; +use stripe::{CreateCustomer, Customer}; -const PRICE_ID: &str = "price_1PZ00000000000000000000"; +use crate::{state::AppState, stripe_mod::ops as stripe_ops}; pub async fn handler( State(state): State, Extension(jwt): Extension, ) -> Result { - let account = state - .admin_db - .get_account_by_clerk_user_id(&jwt.sub) - .await - .unwrap() - .unwrap(); + let (clerk_user_id, clerk_org_id) = (jwt.sub, jwt.org.map(|o| o.id)); + + let account = { + if let Some(clerk_org_id) = &clerk_org_id { + state + .admin_db + .get_account_by_clerk_org_id(clerk_org_id) + .await + } else { + state + .admin_db + .get_account_by_clerk_user_id(&clerk_user_id) + .await + } + } + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? + .ok_or((StatusCode::UNAUTHORIZED, "account_not_found".to_string()))?; let billing = state .admin_db .get_billing_by_account_id(&account.id) .await - .unwrap(); + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let customer_id = match billing.and_then(|b| b.stripe_customer) { Some(c) => c.id, @@ -41,20 +47,9 @@ pub async fn handler( } }; - let checkout_session = { - let mut params = CreateCheckoutSession::new(); - params.cancel_url = Some("http://test.com/cancel"); - params.success_url = Some("http://test.com/success"); - params.customer = Some(customer_id); - params.mode = Some(CheckoutSessionMode::Subscription); - params.line_items = Some(vec![CreateCheckoutSessionLineItems { - quantity: Some(1), - price: Some(PRICE_ID.to_string()), - ..Default::default() - }]); - - CheckoutSession::create(&state.stripe, params).await? - }; + let checkout_session = stripe_ops::create_checkout_without_trial(&state.stripe, customer_id) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok(checkout_session.url.unwrap()) } diff --git a/apps/app/server/src/web/connect.rs b/apps/app/server/src/web/connect.rs index ccfabcd54..d2100ac7a 100644 --- a/apps/app/server/src/web/connect.rs +++ b/apps/app/server/src/web/connect.rs @@ -17,12 +17,16 @@ pub async fn handler( let (clerk_user_id, clerk_org_id) = (jwt.sub, jwt.org.map(|o| o.id)); let existing_account = { - let db = state.admin_db.clone(); - if let Some(clerk_org_id) = &clerk_org_id { - db.get_account_by_clerk_org_id(clerk_org_id).await + state + .admin_db + .get_account_by_clerk_org_id(clerk_org_id) + .await } else { - db.get_account_by_clerk_user_id(&clerk_user_id).await + state + .admin_db + .get_account_by_clerk_user_id(&clerk_user_id) + .await } } .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; From 497de6f1b22736c350cea08eebb02d4f6bbb4479 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 9 May 2025 21:55:15 -0700 Subject: [PATCH 04/26] i18n --- apps/desktop/src/locales/en/messages.po | 14 +++++++++----- apps/desktop/src/locales/ko/messages.po | 14 +++++++++----- apps/docs/data/i18n.json | 6 +++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po index 54f296c84..ab6336704 100644 --- a/apps/desktop/src/locales/en/messages.po +++ b/apps/desktop/src/locales/en/messages.po @@ -237,7 +237,7 @@ msgstr "{0}" msgid "{0} calendars selected" msgstr "{0} calendars selected" -#: src/components/settings/views/lab.tsx:139 +#: src/components/settings/views/lab.tsx:103 msgid "{description}" msgstr "{description}" @@ -245,7 +245,7 @@ msgstr "{description}" #~ msgid "{selectedCount} calendars selected" #~ msgstr "{selectedCount} calendars selected" -#: src/components/settings/views/lab.tsx:136 +#: src/components/settings/views/lab.tsx:100 msgid "{title}" msgstr "{title}" @@ -296,7 +296,7 @@ msgstr "Admin" msgid "AI" msgstr "AI" -#: src/components/login-modal.tsx:97 +#: src/components/login-modal.tsx:118 msgid "AI notepad for meetings" msgstr "AI notepad for meetings" @@ -600,7 +600,7 @@ msgstr "Full name" msgid "General" msgstr "General" -#: src/components/login-modal.tsx:105 +#: src/components/login-modal.tsx:126 msgid "Get Started" msgstr "Get Started" @@ -820,6 +820,10 @@ msgstr "Open Note" msgid "Optional for participant suggestions" msgstr "Optional for participant suggestions" +#: src/components/login-modal.tsx:137 +msgid "or, just use it locally" +msgstr "or, just use it locally" + #: src/components/settings/views/team.tsx:139 #: src/components/settings/views/team.tsx:226 msgid "Owner" @@ -1086,7 +1090,7 @@ msgid "View calendar" msgstr "View calendar" #: src/components/left-sidebar/events-list.tsx:193 -#: src/components/editor-area/note-header/chips/event-chip.tsx:63 +#: src/components/editor-area/note-header/chips/event-chip.tsx:98 msgid "View in calendar" msgstr "View in calendar" diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po index 97a7ce094..1eaa07eff 100644 --- a/apps/desktop/src/locales/ko/messages.po +++ b/apps/desktop/src/locales/ko/messages.po @@ -237,7 +237,7 @@ msgstr "" msgid "{0} calendars selected" msgstr "" -#: src/components/settings/views/lab.tsx:139 +#: src/components/settings/views/lab.tsx:103 msgid "{description}" msgstr "" @@ -245,7 +245,7 @@ msgstr "" #~ msgid "{selectedCount} calendars selected" #~ msgstr "" -#: src/components/settings/views/lab.tsx:136 +#: src/components/settings/views/lab.tsx:100 msgid "{title}" msgstr "" @@ -296,7 +296,7 @@ msgstr "" msgid "AI" msgstr "" -#: src/components/login-modal.tsx:97 +#: src/components/login-modal.tsx:118 msgid "AI notepad for meetings" msgstr "" @@ -600,7 +600,7 @@ msgstr "" msgid "General" msgstr "" -#: src/components/login-modal.tsx:105 +#: src/components/login-modal.tsx:126 msgid "Get Started" msgstr "" @@ -820,6 +820,10 @@ msgstr "" msgid "Optional for participant suggestions" msgstr "" +#: src/components/login-modal.tsx:137 +msgid "or, just use it locally" +msgstr "" + #: src/components/settings/views/team.tsx:139 #: src/components/settings/views/team.tsx:226 msgid "Owner" @@ -1086,7 +1090,7 @@ msgid "View calendar" msgstr "" #: src/components/left-sidebar/events-list.tsx:193 -#: src/components/editor-area/note-header/chips/event-chip.tsx:63 +#: src/components/editor-area/note-header/chips/event-chip.tsx:98 msgid "View in calendar" msgstr "" diff --git a/apps/docs/data/i18n.json b/apps/docs/data/i18n.json index 27f87f759..59888a954 100644 --- a/apps/docs/data/i18n.json +++ b/apps/docs/data/i18n.json @@ -1,12 +1,12 @@ [ { "language": "ko", - "total": 258, - "missing": 258 + "total": 259, + "missing": 259 }, { "language": "en (source)", - "total": 258, + "total": 259, "missing": 0 } ] From ae5a1c9710ef7a102ad4c1e3943dd754a8819d0c Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 9 May 2025 22:17:26 -0700 Subject: [PATCH 05/26] more progress --- apps/app/server/src/stripe/ops.rs | 23 ++++++++++++++--------- apps/app/server/src/web/checkout.rs | 3 +-- apps/app/server/src/web/connect.rs | 14 ++++++++++++-- apps/desktop/src/contexts/hypr.tsx | 9 +++++++-- crates/db-admin/src/billings_ops.rs | 20 +++++++++++--------- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/apps/app/server/src/stripe/ops.rs b/apps/app/server/src/stripe/ops.rs index 15524c1d1..de7f922f7 100644 --- a/apps/app/server/src/stripe/ops.rs +++ b/apps/app/server/src/stripe/ops.rs @@ -3,9 +3,10 @@ use stripe::{ CreateCheckoutSessionLineItems, CreateCheckoutSessionSubscriptionDataTrialSettings, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, - CreateSubscription, CreateSubscriptionTrialSettings, + CreateCustomer, CreateSubscription, CreateSubscriptionTrialSettings, CreateSubscriptionTrialSettingsEndBehavior, - CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, CustomerId, Subscription, + CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, Customer, CustomerId, + Subscription, }; #[cfg(debug_assertions)] @@ -48,16 +49,12 @@ pub async fn create_checkout_without_trial( pub async fn create_subscription_with_trial( client: &Client, customer_id: CustomerId, + trial_period_days: u32, ) -> Result { let mut params = CreateSubscription::new(customer_id); - params.trial_settings = Some(CreateSubscriptionTrialSettings { - end_behavior: CreateSubscriptionTrialSettingsEndBehavior { - missing_payment_method: - CreateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, - }, - }); - params.trial_period_days = Some(7); + params.trial_settings = Some(trial_settings_for_subscription()); + params.trial_period_days = Some(trial_period_days); let subscription = Subscription::create(client, params) .await @@ -66,6 +63,14 @@ pub async fn create_subscription_with_trial( Ok(subscription) } +pub async fn create_customer(client: &Client) -> Result { + let customer = Customer::create(client, CreateCustomer::default()) + .await + .map_err(|e| e.to_string())?; + + Ok(customer) +} + // https://docs.stripe.com/billing/subscriptions/trials#create-free-trials-without-payment fn trial_settings_for_subscription() -> CreateSubscriptionTrialSettings { CreateSubscriptionTrialSettings { diff --git a/apps/app/server/src/web/checkout.rs b/apps/app/server/src/web/checkout.rs index 2c35245f3..8f881dbc2 100644 --- a/apps/app/server/src/web/checkout.rs +++ b/apps/app/server/src/web/checkout.rs @@ -4,7 +4,6 @@ use axum::{ }; use clerk_rs::validators::authorizer::ClerkJwt; -use stripe::{CreateCustomer, Customer}; use crate::{state::AppState, stripe_mod::ops as stripe_ops}; @@ -39,7 +38,7 @@ pub async fn handler( let customer_id = match billing.and_then(|b| b.stripe_customer) { Some(c) => c.id, None => { - let c = Customer::create(&state.stripe, CreateCustomer::default()) + let c = stripe_ops::create_customer(&state.stripe) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; diff --git a/apps/app/server/src/web/connect.rs b/apps/app/server/src/web/connect.rs index d2100ac7a..d326cd7aa 100644 --- a/apps/app/server/src/web/connect.rs +++ b/apps/app/server/src/web/connect.rs @@ -3,10 +3,9 @@ use axum::{ http::StatusCode, Json, }; - -use crate::state::AppState; use clerk_rs::validators::authorizer::ClerkJwt; +use crate::{state::AppState, stripe_mod::ops as stripe_ops}; use hypr_auth_interface::{RequestParams, ResponseParams}; pub async fn handler( @@ -42,6 +41,17 @@ pub async fn handler( // make sure we use same format in tauri side let turso_db_name = hypr_turso::format_db_name(account_id.clone()); + let customer = stripe_ops::create_customer(&state.stripe).await.unwrap(); + let subscription = + stripe_ops::create_subscription_with_trial(&state.stripe, customer.id.clone(), 7) + .await + .unwrap(); + + let _ = db + .create_billing(&account_id, customer, subscription) + .await + .unwrap(); + db.upsert_account(hypr_db_admin::Account { id: account_id, turso_db_name, diff --git a/apps/desktop/src/contexts/hypr.tsx b/apps/desktop/src/contexts/hypr.tsx index 1bae33a8d..db38e7b6f 100644 --- a/apps/desktop/src/contexts/hypr.tsx +++ b/apps/desktop/src/contexts/hypr.tsx @@ -1,18 +1,20 @@ import { useQueries } from "@tanstack/react-query"; import { createContext, useContext } from "react"; +import { getApiDesktopSubscriptionOptions, type Subscription } from "@/client"; import { commands as authCommands } from "@hypr/plugin-auth"; import { commands as dbCommands } from "@hypr/plugin-db"; export interface HyprContext { userId: string; onboardingSessionId: string; + subscription?: Subscription; } const HyprContext = createContext(null); export function HyprProvider({ children }: { children: React.ReactNode }) { - const [userId, onboardingSessionId] = useQueries({ + const [userId, onboardingSessionId, subscription] = useQueries({ queries: [ { queryKey: ["auth-user-id"], @@ -22,6 +24,7 @@ export function HyprProvider({ children }: { children: React.ReactNode }) { queryKey: ["onboarding-session-id"], queryFn: () => dbCommands.onboardingSessionId(), }, + getApiDesktopSubscriptionOptions(), ], }); @@ -39,7 +42,9 @@ export function HyprProvider({ children }: { children: React.ReactNode }) { } return ( - + {children} ); diff --git a/crates/db-admin/src/billings_ops.rs b/crates/db-admin/src/billings_ops.rs index e294e0108..264c9c822 100644 --- a/crates/db-admin/src/billings_ops.rs +++ b/crates/db-admin/src/billings_ops.rs @@ -23,26 +23,28 @@ impl AdminDatabase { } } - pub async fn create_empty_billing( + pub async fn create_billing( &self, account_id: impl Into, + stripe_customer: stripe::Customer, + stripe_subscription: stripe::Subscription, ) -> Result, crate::Error> { let conn = self.conn()?; let mut rows = conn .query( "INSERT INTO billings ( - id, - account_id, - stripe_subscription, - stripe_customer - ) VALUES (?, ?, ?, ?) - RETURNING *", + id, + account_id, + stripe_subscription, + stripe_customer + ) VALUES (?, ?, ?, ?) + RETURNING *", vec![ libsql::Value::Text(uuid::Uuid::new_v4().to_string()), libsql::Value::Text(account_id.into()), - libsql::Value::Null, - libsql::Value::Null, + libsql::Value::Text(serde_json::to_string(&stripe_subscription).unwrap()), + libsql::Value::Text(serde_json::to_string(&stripe_customer).unwrap()), ], ) .await?; From e3d121d2939d9e8bfa44f02c908e60fb231fb09c Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 09:38:08 -0700 Subject: [PATCH 06/26] wip --- apps/app/server/openapi.gen.json | 18 +----------------- apps/app/server/src/main.rs | 4 ---- apps/app/server/src/middleware.rs | 8 ++++++++ apps/app/server/src/native/subscription.rs | 19 ++++++++++--------- apps/app/server/src/types.rs | 14 -------------- packages/client/generated/types.gen.ts | 13 ++++++------- 6 files changed, 25 insertions(+), 51 deletions(-) diff --git a/apps/app/server/openapi.gen.json b/apps/app/server/openapi.gen.json index 9872dbe3d..840b5773f 100644 --- a/apps/app/server/openapi.gen.json +++ b/apps/app/server/openapi.gen.json @@ -1257,14 +1257,6 @@ "mp3" ] }, - "Membership": { - "type": "string", - "enum": [ - "Trial", - "Basic", - "Pro" - ] - }, "NangoConnectSessionRequest": { "type": "object", "required": [ @@ -1687,15 +1679,7 @@ ] }, "Subscription": { - "type": "object", - "required": [ - "membership" - ], - "properties": { - "membership": { - "$ref": "#/components/schemas/Membership" - } - } + "type": "object" }, "TranscriptChunk": { "type": "object", diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index b5a90a283..951f4dced 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -211,10 +211,6 @@ fn main() { AuthState::from_ref(&state), middleware::verify_api_key, )) - .layer(axum::middleware::from_fn_with_state( - AuthState::from_ref(&state), - middleware::attach_user_db, - )) .layer(axum::middleware::from_fn_with_state( AnalyticsState::from_ref(&state), middleware::send_analytics, diff --git a/apps/app/server/src/middleware.rs b/apps/app/server/src/middleware.rs index 8121b3246..28214ca7f 100644 --- a/apps/app/server/src/middleware.rs +++ b/apps/app/server/src/middleware.rs @@ -42,8 +42,15 @@ pub async fn verify_api_key( .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? .ok_or((StatusCode::UNAUTHORIZED, "account_not_found".into()))?; + let billing = state + .admin_db + .get_billing_by_account_id(&account.id) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + req.extensions_mut().insert(user); req.extensions_mut().insert(account); + req.extensions_mut().insert(billing); Ok(next.run(req).await) } @@ -83,6 +90,7 @@ pub async fn attach_user_from_clerk( Ok(next.run(req).await) } +#[allow(unused)] #[tracing::instrument(skip_all)] pub async fn attach_user_db( State(state): State, diff --git a/apps/app/server/src/native/subscription.rs b/apps/app/server/src/native/subscription.rs index ca71f4f08..e4aa9b8b6 100644 --- a/apps/app/server/src/native/subscription.rs +++ b/apps/app/server/src/native/subscription.rs @@ -1,15 +1,16 @@ use axum::{extract::State, http::StatusCode, Extension, Json}; -use crate::{ - state::AppState, - types::{Membership, Subscription}, -}; +use crate::state::AppState; pub async fn handler( - State(_state): State, - Extension(_org): Extension, + Extension(billing): Extension, ) -> Result, StatusCode> { - Ok(Json(Subscription { - membership: Membership::Trial, - })) + // let a = billing.stripe_subscription.map(|s| s.); + + let subscription = Subscription {}; + + Ok(Json(subscription)) } + +#[derive(Debug, serde::Serialize, schemars::JsonSchema)] +pub struct Subscription {} diff --git a/apps/app/server/src/types.rs b/apps/app/server/src/types.rs index 637caa23b..82691a9ae 100644 --- a/apps/app/server/src/types.rs +++ b/apps/app/server/src/types.rs @@ -1,19 +1,5 @@ use serde::{ser::Serializer, Serialize}; -#[derive( - Debug, Clone, serde::Serialize, serde::Deserialize, strum::Display, schemars::JsonSchema, -)] -pub enum Membership { - Trial, - Basic, - Pro, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)] -pub struct Subscription { - pub membership: Membership, -} - #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] diff --git a/packages/client/generated/types.gen.ts b/packages/client/generated/types.gen.ts index 10cd65848..06f914c4b 100644 --- a/packages/client/generated/types.gen.ts +++ b/packages/client/generated/types.gen.ts @@ -1,5 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts +export type Billing = { + account_id: string; + id: string; +}; + export type ChatCompletionAudio = { /** * Specifies the output audio format. Must be one of `wav`, `mp3`, `flac`, `opus`, or `pcm16`. @@ -459,8 +464,6 @@ export type InputAudio = { export type InputAudioFormat = 'wav' | 'mp3'; -export type Membership = 'Trial' | 'Basic' | 'Pro'; - export type NangoConnectSessionRequest = { allowed_integrations: Array; end_user: NangoConnectSessionRequestUser; @@ -579,10 +582,6 @@ export type Session = { export type Stop = string | Array; -export type Subscription = { - membership: Membership; -}; - export type TranscriptChunk = { confidence?: number | null; end: number; @@ -618,7 +617,7 @@ export type GetApiDesktopSubscriptionData = { }; export type GetApiDesktopSubscriptionResponses = { - 200: Subscription; + 200: Billing; }; export type GetApiDesktopSubscriptionResponse = GetApiDesktopSubscriptionResponses[keyof GetApiDesktopSubscriptionResponses]; From d0f775322a4f8a480e71ddb488f179e972d90d57 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 10:45:45 -0700 Subject: [PATCH 07/26] update subscription endpoint --- apps/app/server/openapi.gen.json | 41 +++++++++++++++- apps/app/server/src/native/subscription.rs | 57 +++++++++++++++++++--- packages/client/generated/types.gen.ts | 16 +++--- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/apps/app/server/openapi.gen.json b/apps/app/server/openapi.gen.json index 840b5773f..360f062e2 100644 --- a/apps/app/server/openapi.gen.json +++ b/apps/app/server/openapi.gen.json @@ -1679,7 +1679,46 @@ ] }, "Subscription": { - "type": "object" + "type": "object", + "required": [ + "current_period_end", + "status" + ], + "properties": { + "current_period_end": { + "type": "integer", + "format": "int64" + }, + "plan": { + "type": [ + "string", + "null" + ] + }, + "status": { + "$ref": "#/components/schemas/SubscriptionStatus" + }, + "trial_end": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "SubscriptionStatus": { + "type": "string", + "enum": [ + "active", + "canceled", + "incomplete", + "incomplete_expired", + "past_due", + "paused", + "trialing", + "unpaid" + ] }, "TranscriptChunk": { "type": "object", diff --git a/apps/app/server/src/native/subscription.rs b/apps/app/server/src/native/subscription.rs index e4aa9b8b6..a2e557239 100644 --- a/apps/app/server/src/native/subscription.rs +++ b/apps/app/server/src/native/subscription.rs @@ -1,16 +1,59 @@ -use axum::{extract::State, http::StatusCode, Extension, Json}; - -use crate::state::AppState; +use axum::{http::StatusCode, Extension, Json}; pub async fn handler( Extension(billing): Extension, ) -> Result, StatusCode> { - // let a = billing.stripe_subscription.map(|s| s.); + let subscription = billing.stripe_subscription.map(|s| Subscription { + status: s.status.into(), + current_period_end: s.current_period_end, + trial_end: s.trial_end, + plan: s + .items + .data + .first() + .map(|item| item.price.as_ref().map(|p| p.id.to_string())) + .flatten(), + }); - let subscription = Subscription {}; + if let Some(subscription) = subscription { + Ok(Json(subscription)) + } else { + Err(StatusCode::NOT_FOUND) + } +} - Ok(Json(subscription)) +#[derive(Debug, serde::Serialize, schemars::JsonSchema)] +pub struct Subscription { + pub status: SubscriptionStatus, + pub current_period_end: i64, + pub trial_end: Option, + pub plan: Option, } #[derive(Debug, serde::Serialize, schemars::JsonSchema)] -pub struct Subscription {} +#[serde(rename_all = "snake_case")] +pub enum SubscriptionStatus { + Active, + Canceled, + Incomplete, + IncompleteExpired, + PastDue, + Paused, + Trialing, + Unpaid, +} + +impl From for SubscriptionStatus { + fn from(status: stripe::SubscriptionStatus) -> Self { + match status { + stripe::SubscriptionStatus::Active => SubscriptionStatus::Active, + stripe::SubscriptionStatus::Canceled => SubscriptionStatus::Canceled, + stripe::SubscriptionStatus::Incomplete => SubscriptionStatus::Incomplete, + stripe::SubscriptionStatus::IncompleteExpired => SubscriptionStatus::IncompleteExpired, + stripe::SubscriptionStatus::PastDue => SubscriptionStatus::PastDue, + stripe::SubscriptionStatus::Paused => SubscriptionStatus::Paused, + stripe::SubscriptionStatus::Trialing => SubscriptionStatus::Trialing, + stripe::SubscriptionStatus::Unpaid => SubscriptionStatus::Unpaid, + } + } +} diff --git a/packages/client/generated/types.gen.ts b/packages/client/generated/types.gen.ts index 06f914c4b..bfd6eb6c0 100644 --- a/packages/client/generated/types.gen.ts +++ b/packages/client/generated/types.gen.ts @@ -1,10 +1,5 @@ // This file is auto-generated by @hey-api/openapi-ts -export type Billing = { - account_id: string; - id: string; -}; - export type ChatCompletionAudio = { /** * Specifies the output audio format. Must be one of `wav`, `mp3`, `flac`, `opus`, or `pcm16`. @@ -582,6 +577,15 @@ export type Session = { export type Stop = string | Array; +export type Subscription = { + current_period_end: number; + plan?: string | null; + status: SubscriptionStatus; + trial_end?: number | null; +}; + +export type SubscriptionStatus = 'active' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'past_due' | 'paused' | 'trialing' | 'unpaid'; + export type TranscriptChunk = { confidence?: number | null; end: number; @@ -617,7 +621,7 @@ export type GetApiDesktopSubscriptionData = { }; export type GetApiDesktopSubscriptionResponses = { - 200: Billing; + 200: Subscription; }; export type GetApiDesktopSubscriptionResponse = GetApiDesktopSubscriptionResponses[keyof GetApiDesktopSubscriptionResponses]; From 715f75b38f2e50a4e034559bb887ca4b8c38a8dc Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 13:05:04 -0700 Subject: [PATCH 08/26] fix port --- Taskfile.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index aa14a285c..f25dff432 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -10,7 +10,7 @@ tasks: py:init: POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-cache --no-interaction --all-extras py:run: poetry run python3 {{.CLI_ARGS}} - stripe: stripe listen --skip-verify --forward-to http://localhost:5000/webhook/stripe + stripe: stripe listen --skip-verify --forward-to http://localhost:1234/webhook/stripe bacon: bacon {{.CLI_ARGS}} bump: From b780eb7281cb5312cdff51a6ca717271ae9376c7 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 13:05:34 -0700 Subject: [PATCH 09/26] refactor using envy --- Cargo.lock | 30 ++++++--------- Cargo.toml | 1 + apps/app/server/Cargo.toml | 1 + apps/app/server/src/env.rs | 34 +++++++++++++++++ apps/app/server/src/main.rs | 62 ++++++++++++------------------- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src-tauri/src/env.rs | 11 ++++++ apps/desktop/src-tauri/src/lib.rs | 17 +++------ plugins/analytics/src/lib.rs | 19 +++------- 9 files changed, 92 insertions(+), 85 deletions(-) create mode 100644 apps/app/server/src/env.rs create mode 100644 apps/desktop/src-tauri/src/env.rs diff --git a/Cargo.lock b/Cargo.lock index 3879e24f0..2d25a96d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,7 @@ dependencies = [ "db-core", "db-user", "dotenv", + "envy", "futures-core", "futures-util", "listener-interface", @@ -3311,7 +3312,7 @@ dependencies = [ "data", "db-core", "db-user", - "dotenvy_macro", + "envy", "futures-util", "hound", "reqwest 0.12.15", @@ -3607,24 +3608,6 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dotenvy_macro" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0235d912a8c749f4e0c9f18ca253b4c28cfefc1d2518096016d6e3230b6424" -dependencies = [ - "dotenvy", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "downcast-rs" version = "1.2.1" @@ -3811,6 +3794,15 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index b7fb80fc7..73b43cad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ dirs = "6.0.0" dotenv = "0.15.0" dotenvy = "0.15.7" dotenvy_macro = "0.15.7" +envy = "0.4" include_url_macro = "0.1.0" indoc = "2" insta = "1.42" diff --git a/apps/app/server/Cargo.toml b/apps/app/server/Cargo.toml index b2f4e33e0..bab75d019 100644 --- a/apps/app/server/Cargo.toml +++ b/apps/app/server/Cargo.toml @@ -49,6 +49,7 @@ bytes = { workspace = true } chrono = { workspace = true } codes-iso-639 = { workspace = true } dotenv = { workspace = true } +envy = { workspace = true } thiserror = { workspace = true } url = { workspace = true } uuid = { workspace = true } diff --git a/apps/app/server/src/env.rs b/apps/app/server/src/env.rs new file mode 100644 index 000000000..a52f3a077 --- /dev/null +++ b/apps/app/server/src/env.rs @@ -0,0 +1,34 @@ +pub fn load() -> ENV { + #[cfg(debug_assertions)] + dotenv::from_filename(".env.local").unwrap(); + + envy::from_env::().unwrap() +} + +#[derive(Debug, serde::Deserialize)] +pub struct ENV { + pub sentry_dsn: String, + pub turso_api_key: String, + pub turso_org_slug: String, + pub clerk_secret_key: String, + pub deepgram_api_key: String, + pub clova_api_key: String, + pub turso_admin_db_name: String, + pub nango_api_base: String, + pub nango_api_key: String, + pub posthog_api_key: String, + pub s3_endpoint_url: String, + pub s3_bucket_name: String, + pub s3_access_key_id: String, + pub s3_secret_access_key: String, + pub openai_api_key: String, + pub openai_api_base: String, + pub stripe_secret_key: String, + pub app_static_dir: String, + #[serde(default = "default_port")] + pub port: String, +} + +fn default_port() -> String { + "1234".to_string() +} diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index 951f4dced..75455bd22 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -1,3 +1,4 @@ +mod env; mod error; mod middleware; mod nango; @@ -45,11 +46,10 @@ use clerk_rs::{ use state::{AnalyticsState, AuthState, WorkerState}; fn main() { - #[cfg(debug_assertions)] - dotenv::from_filename(".env.local").unwrap(); + let config = env::load(); let _guard = sentry::init(( - get_env("SENTRY_DSN"), + config.sentry_dsn.clone(), sentry::ClientOptions { release: sentry::release_name!(), ..Default::default() @@ -62,23 +62,11 @@ fn main() { .unwrap() .block_on(async { let layer = { - #[cfg(debug_assertions)] { tracing_subscriber::fmt::layer() .with_file(true) .with_line_number(true) } - - #[cfg(not(debug_assertions))] - { - tracing_axiom::builder("hyprnote-server") - .with_token(get_env("AXIOM_TOKEN")) - .unwrap() - .with_dataset(get_env("AXIOM_DATASET")) - .unwrap() - .build() - .unwrap() - } }; Registry::default() @@ -100,29 +88,29 @@ fn main() { let turso = hypr_turso::TursoClient::builder() .with_token_cache(128) - .api_key(get_env("TURSO_API_KEY")) - .org_slug(get_env("TURSO_ORG_SLUG")) + .api_key(&config.turso_api_key) + .org_slug(&config.turso_org_slug) .build(); let clerk_config = - ClerkConfiguration::new(None, None, Some(get_env("CLERK_SECRET_KEY")), None); + ClerkConfiguration::new(None, None, Some(config.clerk_secret_key.clone()), None); let clerk = Clerk::new(clerk_config); let realtime_stt = hypr_stt::realtime::Client::builder() - .deepgram_api_key(get_env("DEEPGRAM_API_KEY")) - .clova_api_key(get_env("CLOVA_API_KEY")) + .deepgram_api_key(&config.deepgram_api_key) + .clova_api_key(&config.clova_api_key) .build(); let recorded_stt = hypr_stt::recorded::Client::builder() - .deepgram_api_key(get_env("DEEPGRAM_API_KEY")) - .clova_api_key(get_env("CLOVA_API_KEY")) + .deepgram_api_key(&config.deepgram_api_key) + .clova_api_key(&config.clova_api_key) .build(); let admin_db = { let base_db = { - let name = get_env("TURSO_ADMIN_DB_NAME"); + let name = &config.turso_admin_db_name; - let db_name = turso.format_db_name(&name); + let db_name = turso.format_db_name(name); let db_url = turso.format_db_url(&db_name); let db_token = turso.generate_db_token(&db_name).await.unwrap(); @@ -140,25 +128,25 @@ fn main() { }; let nango = hypr_nango::NangoClientBuilder::default() - .api_base(get_env("NANGO_API_BASE")) - .api_key(get_env("NANGO_API_KEY")) + .api_base(&config.nango_api_base) + .api_key(&config.nango_api_key) .build(); - let analytics = hypr_analytics::AnalyticsClient::new(get_env("POSTHOG_API_KEY")); + let analytics = hypr_analytics::AnalyticsClient::new(&config.posthog_api_key); let s3 = hypr_s3::Client::builder() - .endpoint_url(get_env("S3_ENDPOINT_URL")) - .bucket(get_env("S3_BUCKET_NAME")) - .credentials(get_env("S3_ACCESS_KEY_ID"), get_env("S3_SECRET_ACCESS_KEY")) + .endpoint_url(&config.s3_endpoint_url) + .bucket(&config.s3_bucket_name) + .credentials(&config.s3_access_key_id, &config.s3_secret_access_key) .build() .await; let openai = hypr_openai::OpenAIClient::builder() - .api_key(get_env("OPENAI_API_KEY")) - .api_base(get_env("OPENAI_API_BASE")) + .api_key(&config.openai_api_key) + .api_base(&config.openai_api_base) .build(); - let stripe_client = stripe::Client::new(get_env("STRIPE_SECRET_KEY")); + let stripe_client = stripe::Client::new(&config.stripe_secret_key); let state = state::AppState { clerk: clerk.clone(), @@ -250,7 +238,7 @@ fn main() { { router = router.fallback_service({ - let static_dir: std::path::PathBuf = get_env("APP_STATIC_DIR").into(); + let static_dir: std::path::PathBuf = config.app_static_dir.clone().into(); ServeDir::new(&static_dir) .append_index_html_on_directories(false) @@ -260,7 +248,7 @@ fn main() { let mut api = OpenApi::default(); - let port = std::env::var("PORT").unwrap_or("1234".to_string()); + let port = &config.port; let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)) .await .unwrap(); @@ -302,10 +290,6 @@ fn main() { }); } -fn get_env(key: &str) -> String { - std::env::var(key).unwrap_or_else(|_| panic!("env: '{}' is not set", key)) -} - #[cfg(test)] mod tests { #[test] diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 0f74c637a..6bc8c06e1 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -84,7 +84,7 @@ serde_json = { workspace = true } bytes = { workspace = true } chrono = { workspace = true } codes-iso-639 = { workspace = true } -dotenvy_macro = { workspace = true } +envy = { workspace = true } url = { workspace = true } uuid = { workspace = true } diff --git a/apps/desktop/src-tauri/src/env.rs b/apps/desktop/src-tauri/src/env.rs new file mode 100644 index 000000000..40f89d602 --- /dev/null +++ b/apps/desktop/src-tauri/src/env.rs @@ -0,0 +1,11 @@ +#[derive(Debug, serde::Deserialize)] +pub struct ENV { + #[cfg_attr(debug_assertions, serde(default))] + pub sentry_dsn: String, + #[cfg_attr(debug_assertions, serde(default))] + pub posthog_api_key: String, +} + +pub fn load() -> ENV { + envy::from_env::().unwrap() +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 26028756f..ee8650eda 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -1,4 +1,5 @@ mod commands; +mod env; mod ext; mod store; @@ -15,6 +16,8 @@ use tracing_subscriber::{ pub async fn main() { tauri::async_runtime::set(tokio::runtime::Handle::current()); + let config = env::load(); + { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); @@ -27,17 +30,7 @@ pub async fn main() { } let client = tauri_plugin_sentry::sentry::init(( - { - #[cfg(not(debug_assertions))] - { - env!("SENTRY_DSN") - } - - #[cfg(debug_assertions)] - { - option_env!("SENTRY_DSN").unwrap_or_default() - } - }, + config.sentry_dsn, tauri_plugin_sentry::sentry::ClientOptions { release: tauri_plugin_sentry::sentry::release_name!(), traces_sample_rate: 1.0, @@ -83,7 +76,7 @@ pub async fn main() { .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_analytics::init()) + .plugin(tauri_plugin_analytics::init(config.posthog_api_key)) .plugin(tauri_plugin_tray::init()) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_clipboard_manager::init()) diff --git a/plugins/analytics/src/lib.rs b/plugins/analytics/src/lib.rs index fcd134356..1daaeb57f 100644 --- a/plugins/analytics/src/lib.rs +++ b/plugins/analytics/src/lib.rs @@ -24,24 +24,12 @@ fn make_specta_builder() -> tauri_specta::Builder { .error_handling(tauri_specta::ErrorHandlingMode::Throw) } -pub fn init() -> tauri::plugin::TauriPlugin { +pub fn init(api_key: String) -> tauri::plugin::TauriPlugin { let specta_builder = make_specta_builder(); tauri::plugin::Builder::new(PLUGIN_NAME) .invoke_handler(specta_builder.invoke_handler()) .setup(|app, _api| { - let api_key = { - #[cfg(not(debug_assertions))] - { - env!("POSTHOG_API_KEY") - } - - #[cfg(debug_assertions)] - { - option_env!("POSTHOG_API_KEY").unwrap_or_default() - } - }; - let client = hypr_analytics::AnalyticsClient::new(api_key); assert!(app.manage(client)); Ok(()) @@ -71,7 +59,10 @@ mod test { ctx.config_mut().identifier = "com.hyprnote.dev".to_string(); ctx.config_mut().version = Some("0.0.1".to_string()); - builder.plugin(init()).build(ctx).unwrap() + builder + .plugin(init("API_KEY".to_string())) + .build(ctx) + .unwrap() } #[test] From 5e5d0820173c8ce60e18b6c895b5bf45d9a056a2 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 13:25:17 -0700 Subject: [PATCH 10/26] update price id etc --- apps/app/server/openapi.gen.json | 2 +- apps/app/server/src/native/subscription.rs | 4 ++-- apps/app/server/src/stripe/ops.rs | 24 +++++--------------- apps/app/src/routeTree.gen.ts | 26 ++++++++++++++++++++++ apps/app/src/routes/checkout.success.tsx | 9 ++++++++ packages/client/generated/types.gen.ts | 2 +- 6 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 apps/app/src/routes/checkout.success.tsx diff --git a/apps/app/server/openapi.gen.json b/apps/app/server/openapi.gen.json index 360f062e2..7938053c1 100644 --- a/apps/app/server/openapi.gen.json +++ b/apps/app/server/openapi.gen.json @@ -1689,7 +1689,7 @@ "type": "integer", "format": "int64" }, - "plan": { + "price_id": { "type": [ "string", "null" diff --git a/apps/app/server/src/native/subscription.rs b/apps/app/server/src/native/subscription.rs index a2e557239..f92d964a4 100644 --- a/apps/app/server/src/native/subscription.rs +++ b/apps/app/server/src/native/subscription.rs @@ -7,7 +7,7 @@ pub async fn handler( status: s.status.into(), current_period_end: s.current_period_end, trial_end: s.trial_end, - plan: s + price_id: s .items .data .first() @@ -27,7 +27,7 @@ pub struct Subscription { pub status: SubscriptionStatus, pub current_period_end: i64, pub trial_end: Option, - pub plan: Option, + pub price_id: Option, } #[derive(Debug, serde::Serialize, schemars::JsonSchema)] diff --git a/apps/app/server/src/stripe/ops.rs b/apps/app/server/src/stripe/ops.rs index de7f922f7..2f6b305d4 100644 --- a/apps/app/server/src/stripe/ops.rs +++ b/apps/app/server/src/stripe/ops.rs @@ -9,16 +9,6 @@ use stripe::{ Subscription, }; -#[cfg(debug_assertions)] -pub fn get_price_id() -> String { - "price_1PZ00000000000000000000".to_string() -} - -#[cfg(not(debug_assertions))] -pub fn get_price_id() -> String { - "price_1PZ00000000000000000000".to_string() -} - pub fn get_line_items() -> Vec { vec![CreateCheckoutSessionLineItems { quantity: Some(1), @@ -36,9 +26,7 @@ pub async fn create_checkout_without_trial( params.mode = Some(CheckoutSessionMode::Subscription); params.line_items = Some(get_line_items()); - let cancel_url = get_cancel_url(); let success_url = get_success_url(); - params.cancel_url = Some(&cancel_url); params.success_url = Some(&success_url); CheckoutSession::create(client, params) @@ -91,21 +79,21 @@ fn trial_settings_for_checkout() -> CreateCheckoutSessionSubscriptionDataTrialSe } #[cfg(debug_assertions)] -fn get_cancel_url() -> String { - "http://test.com/cancel".to_string() +pub fn get_price_id() -> String { + "price_1RNJnZEABq1oJeLyqULb2gtm".to_string() } #[cfg(not(debug_assertions))] -fn get_cancel_url() -> String { - "http://test.com/cancel".to_string() +pub fn get_price_id() -> String { + "price_1RMxR4EABq1oJeLyOpEFuV2Q".to_string() } #[cfg(debug_assertions)] fn get_success_url() -> String { - "http://test.com/success".to_string() + "http://127.0.0.1:1234/checkout/success".to_string() } #[cfg(not(debug_assertions))] fn get_success_url() -> String { - "http://test.com/success".to_string() + "https://app.hyprnote.com/checkout/success".to_string() } diff --git a/apps/app/src/routeTree.gen.ts b/apps/app/src/routeTree.gen.ts index fce054ecb..447b236bb 100644 --- a/apps/app/src/routeTree.gen.ts +++ b/apps/app/src/routeTree.gen.ts @@ -14,6 +14,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as IntegrationImport } from './routes/integration' import { Route as IndexImport } from './routes/index' import { Route as SIdImport } from './routes/s.$id' +import { Route as CheckoutSuccessImport } from './routes/checkout.success' import { Route as AuthSsoCallbackImport } from './routes/auth.sso-callback' import { Route as AuthSignOutImport } from './routes/auth.sign-out' import { Route as AuthSignInImport } from './routes/auth.sign-in' @@ -39,6 +40,12 @@ const SIdRoute = SIdImport.update({ getParentRoute: () => rootRoute, } as any) +const CheckoutSuccessRoute = CheckoutSuccessImport.update({ + id: '/checkout/success', + path: '/checkout/success', + getParentRoute: () => rootRoute, +} as any) + const AuthSsoCallbackRoute = AuthSsoCallbackImport.update({ id: '/auth/sso-callback', path: '/auth/sso-callback', @@ -109,6 +116,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthSsoCallbackImport parentRoute: typeof rootRoute } + '/checkout/success': { + id: '/checkout/success' + path: '/checkout/success' + fullPath: '/checkout/success' + preLoaderRoute: typeof CheckoutSuccessImport + parentRoute: typeof rootRoute + } '/s/$id': { id: '/s/$id' path: '/s/$id' @@ -128,6 +142,7 @@ export interface FileRoutesByFullPath { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-out': typeof AuthSignOutRoute '/auth/sso-callback': typeof AuthSsoCallbackRoute + '/checkout/success': typeof CheckoutSuccessRoute '/s/$id': typeof SIdRoute } @@ -138,6 +153,7 @@ export interface FileRoutesByTo { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-out': typeof AuthSignOutRoute '/auth/sso-callback': typeof AuthSsoCallbackRoute + '/checkout/success': typeof CheckoutSuccessRoute '/s/$id': typeof SIdRoute } @@ -149,6 +165,7 @@ export interface FileRoutesById { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-out': typeof AuthSignOutRoute '/auth/sso-callback': typeof AuthSsoCallbackRoute + '/checkout/success': typeof CheckoutSuccessRoute '/s/$id': typeof SIdRoute } @@ -161,6 +178,7 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-out' | '/auth/sso-callback' + | '/checkout/success' | '/s/$id' fileRoutesByTo: FileRoutesByTo to: @@ -170,6 +188,7 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-out' | '/auth/sso-callback' + | '/checkout/success' | '/s/$id' id: | '__root__' @@ -179,6 +198,7 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-out' | '/auth/sso-callback' + | '/checkout/success' | '/s/$id' fileRoutesById: FileRoutesById } @@ -190,6 +210,7 @@ export interface RootRouteChildren { AuthSignInRoute: typeof AuthSignInRoute AuthSignOutRoute: typeof AuthSignOutRoute AuthSsoCallbackRoute: typeof AuthSsoCallbackRoute + CheckoutSuccessRoute: typeof CheckoutSuccessRoute SIdRoute: typeof SIdRoute } @@ -200,6 +221,7 @@ const rootRouteChildren: RootRouteChildren = { AuthSignInRoute: AuthSignInRoute, AuthSignOutRoute: AuthSignOutRoute, AuthSsoCallbackRoute: AuthSsoCallbackRoute, + CheckoutSuccessRoute: CheckoutSuccessRoute, SIdRoute: SIdRoute, } @@ -219,6 +241,7 @@ export const routeTree = rootRoute "/auth/sign-in", "/auth/sign-out", "/auth/sso-callback", + "/checkout/success", "/s/$id" ] }, @@ -240,6 +263,9 @@ export const routeTree = rootRoute "/auth/sso-callback": { "filePath": "auth.sso-callback.tsx" }, + "/checkout/success": { + "filePath": "checkout.success.tsx" + }, "/s/$id": { "filePath": "s.$id.tsx" } diff --git a/apps/app/src/routes/checkout.success.tsx b/apps/app/src/routes/checkout.success.tsx new file mode 100644 index 000000000..897896c34 --- /dev/null +++ b/apps/app/src/routes/checkout.success.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/checkout/success")({ + component: Component, +}); + +function Component() { + return
Hello "/checkout/success"!
; +} diff --git a/packages/client/generated/types.gen.ts b/packages/client/generated/types.gen.ts index bfd6eb6c0..81154f05c 100644 --- a/packages/client/generated/types.gen.ts +++ b/packages/client/generated/types.gen.ts @@ -579,7 +579,7 @@ export type Stop = string | Array; export type Subscription = { current_period_end: number; - plan?: string | null; + price_id?: string | null; status: SubscriptionStatus; trial_end?: number | null; }; From 5d83a238168cedec295dded97f7b08609b1b1c98 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 13:48:25 -0700 Subject: [PATCH 11/26] add webhook secret --- apps/app/server/src/env.rs | 1 + apps/app/server/src/main.rs | 7 +++--- apps/app/server/src/nango.rs | 4 ++-- apps/app/server/src/state.rs | 20 ++++++++++++++++ apps/app/server/src/stripe/webhook.rs | 33 ++++++++++++++------------- 5 files changed, 43 insertions(+), 22 deletions(-) diff --git a/apps/app/server/src/env.rs b/apps/app/server/src/env.rs index a52f3a077..9aebce6f6 100644 --- a/apps/app/server/src/env.rs +++ b/apps/app/server/src/env.rs @@ -24,6 +24,7 @@ pub struct ENV { pub openai_api_key: String, pub openai_api_base: String, pub stripe_secret_key: String, + pub stripe_webhook_signing_secret: String, pub app_static_dir: String, #[serde(default = "default_port")] pub port: String, diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index 75455bd22..535cb047a 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -159,6 +159,7 @@ fn main() { s3, openai, stripe: stripe_client, + stripe_webhook_signing_secret: config.stripe_webhook_signing_secret, }; let web_connect_router = @@ -205,12 +206,10 @@ fn main() { )), ); - let slack_router = ApiRouter::new(); - let webhook_router = ApiRouter::new() - .route("/nango", post(nango::handler)) .route("/stripe", post(stripe_mod::webhook::handler)) - .nest("/slack", slack_router); + .route("/nango", post(nango::handler)) + .with_state(state::WebhookState::from_ref(&state)); let mut router = ApiRouter::new() .route("/openapi.json", get(openapi::handler)) diff --git a/apps/app/server/src/nango.rs b/apps/app/server/src/nango.rs index 20e516f76..79b985cb8 100644 --- a/apps/app/server/src/nango.rs +++ b/apps/app/server/src/nango.rs @@ -4,11 +4,11 @@ use axum::{ response::IntoResponse, }; -use crate::state::AppState; +use crate::state::WebhookState; // https://docs.nango.dev/guides/webhooks/webhooks-from-nango#auth-webhooks pub async fn handler( - State(state): State, + State(state): State, Json(input): Json, ) -> Result { let connection = state diff --git a/apps/app/server/src/state.rs b/apps/app/server/src/state.rs index a95e93294..672fd6985 100644 --- a/apps/app/server/src/state.rs +++ b/apps/app/server/src/state.rs @@ -20,6 +20,15 @@ pub struct AppState { pub nango: NangoClient, pub s3: S3Client, pub stripe: stripe::Client, + pub stripe_webhook_signing_secret: String, +} + +#[derive(Clone)] +pub struct WebhookState { + pub nango: NangoClient, + pub admin_db: AdminDatabase, + pub stripe: stripe::Client, + pub stripe_webhook_signing_secret: String, } #[derive(Clone)] @@ -48,6 +57,17 @@ pub struct AnalyticsState { pub analytics: AnalyticsClient, } +impl FromRef for WebhookState { + fn from_ref(s: &AppState) -> WebhookState { + WebhookState { + nango: s.nango.clone(), + admin_db: s.admin_db.clone(), + stripe: s.stripe.clone(), + stripe_webhook_signing_secret: s.stripe_webhook_signing_secret.clone(), + } + } +} + impl FromRef for STTState { fn from_ref(app_state: &AppState) -> STTState { STTState { diff --git a/apps/app/server/src/stripe/webhook.rs b/apps/app/server/src/stripe/webhook.rs index 5d5282130..12a5cd55b 100644 --- a/apps/app/server/src/stripe/webhook.rs +++ b/apps/app/server/src/stripe/webhook.rs @@ -8,19 +8,18 @@ use axum::{ }; use stripe::{CustomerId, Event, EventObject, EventType, Expandable, Object}; -use crate::state::AppState; +use crate::state::WebhookState; // https://github.com/arlyon/async-stripe/blob/c71a7eb/examples/webhook-axum.rs pub struct StripeEvent(Event); -impl FromRequest for StripeEvent -where - String: FromRequest, - S: Send + Sync, -{ +impl FromRequest for StripeEvent { type Rejection = Response; - async fn from_request(req: Request, state: &S) -> Result { + async fn from_request( + req: Request, + state: &WebhookState, + ) -> Result { let signature = if let Some(sig) = req.headers().get("stripe-signature") { sig.to_owned() } else { @@ -32,8 +31,12 @@ where .map_err(IntoResponse::into_response)?; Ok(Self( - stripe::Webhook::construct_event(&payload, signature.to_str().unwrap(), "whsec_xxxxx") - .map_err(|_| StatusCode::BAD_REQUEST.into_response())?, + stripe::Webhook::construct_event( + &payload, + signature.to_str().unwrap(), + &state.stripe_webhook_signing_secret, + ) + .map_err(|_| StatusCode::BAD_REQUEST.into_response())?, )) } } @@ -41,7 +44,7 @@ where // https://github.com/t3dotgg/stripe-recommendations // https://docs.stripe.com/api/events/types pub async fn handler( - State(state): State, + State(state): State, StripeEvent(event): StripeEvent, ) -> impl IntoResponse { match event.type_ { @@ -96,12 +99,9 @@ pub async fn handler( // TODO: do this with background worker with concurrency limit if let Some(stripe_customer_id) = stripe_customer_id { tokio::spawn({ - let stripe_client = state.stripe.clone(); - let admin_db = state.admin_db.clone(); - async move { let customer = stripe::Customer::retrieve( - &stripe_client, + &state.stripe, CustomerId::from_str(&stripe_customer_id).as_ref().unwrap(), &["subscriptions"], ) @@ -110,14 +110,15 @@ pub async fn handler( let customer_id = customer.id().to_string(); - if let Err(e) = admin_db.update_stripe_customer(&customer).await { + if let Err(e) = state.admin_db.update_stripe_customer(&customer).await { tracing::error!("stripe_customer_update_failed: {:?}", e); } let subscriptions = customer.subscriptions.unwrap_or_default(); let subscription = subscriptions.data.first(); - if let Err(e) = admin_db + if let Err(e) = state + .admin_db .update_stripe_subscription(customer_id, subscription) .await { From e9aa4898bd7e68451acdae3786ba34dcbd8800c9 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 13:48:32 -0700 Subject: [PATCH 12/26] fix typo --- apps/desktop/src/contexts/hypr.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/contexts/hypr.tsx b/apps/desktop/src/contexts/hypr.tsx index db38e7b6f..2c63e6a26 100644 --- a/apps/desktop/src/contexts/hypr.tsx +++ b/apps/desktop/src/contexts/hypr.tsx @@ -53,7 +53,7 @@ export function HyprProvider({ children }: { children: React.ReactNode }) { export function useHypr() { const context = useContext(HyprContext); if (!context) { - throw new Error("useHypr must be used within an AuthProvider"); + throw new Error("useHypr must be used within an HyprProvider"); } return context; } From 85ba4c570385c64d8547f190a68706730a4a36e3 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 14:08:57 -0700 Subject: [PATCH 13/26] will display billing in plans route --- .../settings/components/settings-header.tsx | 2 - .../src/components/settings/views/billing.tsx | 208 ------------------ .../src/components/settings/views/index.ts | 1 - apps/desktop/src/locales/en/messages.po | 119 +++++----- apps/desktop/src/locales/ko/messages.po | 119 +++++----- apps/desktop/src/routes/app.plans.tsx | 109 ++++++++- 6 files changed, 214 insertions(+), 344 deletions(-) delete mode 100644 apps/desktop/src/components/settings/views/billing.tsx diff --git a/apps/desktop/src/components/settings/components/settings-header.tsx b/apps/desktop/src/components/settings/components/settings-header.tsx index 77ab77bce..3a6f130c4 100644 --- a/apps/desktop/src/components/settings/components/settings-header.tsx +++ b/apps/desktop/src/components/settings/components/settings-header.tsx @@ -32,8 +32,6 @@ export function SettingsHeader({ current, onCreateTemplate }: SettingsHeaderProp return t`Extensions`; case "team": return t`Team`; - case "billing": - return t`Billing`; default: return tab; } diff --git a/apps/desktop/src/components/settings/views/billing.tsx b/apps/desktop/src/components/settings/views/billing.tsx deleted file mode 100644 index 5049c75da..000000000 --- a/apps/desktop/src/components/settings/views/billing.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { Trans, useLingui } from "@lingui/react/macro"; -import { CheckIcon, ExternalLinkIcon } from "lucide-react"; -import { useState } from "react"; - -import { Button } from "@hypr/ui/components/ui/button"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@hypr/ui/components/ui/card"; -import { Tabs, TabsList, TabsTrigger } from "@hypr/ui/components/ui/tabs"; - -interface BillingProps { - currentPlan?: string; - trialDaysLeft?: number; -} - -export default function Billing({ currentPlan, trialDaysLeft }: BillingProps) { - const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly"); - const { t } = useLingui(); - - const pricingPlans = [ - { - name: t`Free`, - description: t`For those who are serious about their privacy`, - monthlyPrice: 0, - annualPrice: 0, - features: [ - t`Works both in-person and remotely`, - t`Format notes using templates`, - t`Ask questions about past meetings`, - t`Live summary of the meeting`, - t`Works offline`, - ], - }, - { - name: t`Pro`, - description: t`For those who are serious about their performance`, - monthlyPrice: 19, - annualPrice: 15, - features: [ - t`Integration with other apps like Notion and Google Calendar`, - t`Long-term memory for past meetings and attendees`, - t`Much better AI performance`, - t`Meeting note sharing via links`, - t`Synchronization across multiple devices`, - ], - }, - { - name: t`Team`, - description: t`For fast growing teams like energetic startups`, - monthlyPrice: 25, - annualPrice: 20, - features: [ - t`Search & ask across all notes in workspace`, - t`Collaborate with others in meetings`, - t`Single sign-on for all users`, - ], - isPerSeat: true, - comingSoon: true, - }, - ]; - - const getButtonText = (planName: string) => { - const plan = planName.toLowerCase(); - if (plan === "team") { - return t`Coming Soon`; - } - if (currentPlan === plan) { - return t`Current Plan`; - } - if (currentPlan === "basic" && plan === "pro") { - return t`Upgrade`; - } - if (trialDaysLeft && plan === "pro") { - return t`Free Trial`; - } - return billingCycle === "monthly" - ? t`Start Monthly Plan` - : t`Start Annual Plan`; - }; - - const getButtonProps = (planName: string) => { - const plan = planName.toLowerCase(); - if (plan === "team") { - return { - disabled: true, - variant: "outline" as const, - }; - } - if (currentPlan === plan) { - return { - variant: "outline" as const, - }; - } - return { - variant: "default" as const, - }; - }; - - return ( -
-
-
- Coming Soon -
-

- - Billing features are currently under development and will be available in a future update. - -

-
- -
-
-

- There's a plan for everyone -

- - setBillingCycle(value as "monthly" | "annual")} - > - - - Monthly - - - Annual - - - -
- -
-
- {pricingPlans.map((plan) => ( - - -
- {plan.name} - {plan.name === "Pro" && ( - - Best - - )} -
- {plan.description} -
- -
- $ - {billingCycle === "monthly" - ? plan.monthlyPrice - : plan.annualPrice} - - {plan.isPerSeat ? "/seat" : ""} /month - -
-
- {plan.features.map((feature) => ( -
-
- -
- {feature} -
- ))} -
-
- - - {trialDaysLeft && plan.name.toLowerCase() === "pro" && ( -

- {trialDaysLeft} days left in trial -

- )} -
-
- ))} -
- - {billingCycle === "annual" && ( -

- Save up to 20% with annual billing -

- )} - - -
-
-
- ); -} diff --git a/apps/desktop/src/components/settings/views/index.ts b/apps/desktop/src/components/settings/views/index.ts index c3e06e7ac..4393c87f0 100644 --- a/apps/desktop/src/components/settings/views/index.ts +++ b/apps/desktop/src/components/settings/views/index.ts @@ -1,5 +1,4 @@ export { default as LocalAI } from "./ai"; -export { default as Billing } from "./billing"; export { default as Calendar } from "./calendar"; export { default as Extensions } from "./extension"; export { default as Feedback } from "./feedback"; diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po index ab6336704..c0514de8f 100644 --- a/apps/desktop/src/locales/en/messages.po +++ b/apps/desktop/src/locales/en/messages.po @@ -310,8 +310,8 @@ msgid "and {0} more members" msgstr "and {0} more members" #: src/components/settings/views/billing.tsx:131 -msgid "Annual" -msgstr "Annual" +#~ msgid "Annual" +#~ msgstr "Annual" #: src/components/share-and-permission/publish.tsx:18 msgid "Anyone with the link can view this page" @@ -339,8 +339,8 @@ msgid "Are you sure you want to delete this note?" msgstr "Are you sure you want to delete this note?" #: src/components/settings/views/billing.tsx:27 -msgid "Ask questions about past meetings" -msgstr "Ask questions about past meetings" +#~ msgid "Ask questions about past meetings" +#~ msgstr "Ask questions about past meetings" #: src/components/right-panel/components/chat/chat-message.tsx:18 msgid "Assistant:" @@ -351,12 +351,12 @@ msgid "Back to Settings" msgstr "Back to Settings" #: src/components/settings/components/settings-header.tsx:36 -msgid "Billing" -msgstr "Billing" +#~ msgid "Billing" +#~ msgstr "Billing" #: src/components/settings/views/billing.tsx:104 -msgid "Billing features are currently under development and will be available in a future update." -msgstr "Billing features are currently under development and will be available in a future update." +#~ msgid "Billing features are currently under development and will be available in a future update." +#~ msgstr "Billing features are currently under development and will be available in a future update." #: src/components/settings/components/templates-sidebar.tsx:68 msgid "Built-in Templates" @@ -392,12 +392,10 @@ msgid "Choose whether to save your recordings locally." msgstr "Choose whether to save your recordings locally." #: src/components/settings/views/billing.tsx:52 -msgid "Collaborate with others in meetings" -msgstr "Collaborate with others in meetings" +#~ msgid "Collaborate with others in meetings" +#~ msgstr "Collaborate with others in meetings" #: src/components/settings/views/team.tsx:69 -#: src/components/settings/views/billing.tsx:63 -#: src/components/settings/views/billing.tsx:101 #: src/components/settings/components/extensions-sidebar.tsx:55 msgid "Coming Soon" msgstr "Coming Soon" @@ -453,8 +451,8 @@ msgid "Create Note" msgstr "Create Note" #: src/components/settings/views/billing.tsx:66 -msgid "Current Plan" -msgstr "Current Plan" +#~ msgid "Current Plan" +#~ msgstr "Current Plan" #: src/components/settings/views/ai.tsx:451 msgid "Custom Endpoint" @@ -568,28 +566,28 @@ msgid "Feedback" msgstr "Feedback" #: src/components/settings/views/billing.tsx:47 -msgid "For fast growing teams like energetic startups" -msgstr "For fast growing teams like energetic startups" +#~ msgid "For fast growing teams like energetic startups" +#~ msgstr "For fast growing teams like energetic startups" #: src/components/settings/views/billing.tsx:34 -msgid "For those who are serious about their performance" -msgstr "For those who are serious about their performance" +#~ msgid "For those who are serious about their performance" +#~ msgstr "For those who are serious about their performance" #: src/components/settings/views/billing.tsx:21 -msgid "For those who are serious about their privacy" -msgstr "For those who are serious about their privacy" +#~ msgid "For those who are serious about their privacy" +#~ msgstr "For those who are serious about their privacy" #: src/components/settings/views/billing.tsx:26 -msgid "Format notes using templates" -msgstr "Format notes using templates" +#~ msgid "Format notes using templates" +#~ msgstr "Format notes using templates" #: src/components/settings/views/billing.tsx:20 -msgid "Free" -msgstr "Free" +#~ msgid "Free" +#~ msgstr "Free" #: src/components/settings/views/billing.tsx:72 -msgid "Free Trial" -msgstr "Free Trial" +#~ msgid "Free Trial" +#~ msgstr "Free Trial" #: src/components/settings/views/profile.tsx:119 msgid "Full name" @@ -630,8 +628,8 @@ msgstr "How can I help you today?" #~ msgstr "Instruction" #: src/components/settings/views/billing.tsx:38 -msgid "Integration with other apps like Notion and Google Calendar" -msgstr "Integration with other apps like Notion and Google Calendar" +#~ msgid "Integration with other apps like Notion and Google Calendar" +#~ msgstr "Integration with other apps like Notion and Google Calendar" #: src/components/share-and-permission/invite-list.tsx:30 msgid "Invite" @@ -666,16 +664,16 @@ msgstr "Language" #~ msgstr "Language Model" #: src/components/settings/views/billing.tsx:200 -msgid "Learn more about our pricing plans" -msgstr "Learn more about our pricing plans" +#~ msgid "Learn more about our pricing plans" +#~ msgstr "Learn more about our pricing plans" #: src/components/settings/views/profile.tsx:210 msgid "LinkedIn username" msgstr "LinkedIn username" #: src/components/settings/views/billing.tsx:28 -msgid "Live summary of the meeting" -msgstr "Live summary of the meeting" +#~ msgid "Live summary of the meeting" +#~ msgstr "Live summary of the meeting" #: src/components/settings/views/extension.tsx:118 msgid "Loading extension details..." @@ -691,16 +689,16 @@ msgid "Local mode" msgstr "Local mode" #: src/components/settings/views/billing.tsx:39 -msgid "Long-term memory for past meetings and attendees" -msgstr "Long-term memory for past meetings and attendees" +#~ msgid "Long-term memory for past meetings and attendees" +#~ msgstr "Long-term memory for past meetings and attendees" #: src/components/share-and-permission/publish.tsx:24 msgid "Make it public" msgstr "Make it public" #: src/components/settings/views/billing.tsx:41 -msgid "Meeting note sharing via links" -msgstr "Meeting note sharing via links" +#~ msgid "Meeting note sharing via links" +#~ msgstr "Meeting note sharing via links" #: src/components/settings/views/team.tsx:145 #: src/components/settings/views/team.tsx:232 @@ -724,12 +722,12 @@ msgid "Model Name" msgstr "Model Name" #: src/components/settings/views/billing.tsx:125 -msgid "Monthly" -msgstr "Monthly" +#~ msgid "Monthly" +#~ msgstr "Monthly" #: src/components/settings/views/billing.tsx:40 -msgid "Much better AI performance" -msgstr "Much better AI performance" +#~ msgid "Much better AI performance" +#~ msgstr "Much better AI performance" #: src/components/left-sidebar/top-area/settings-button.tsx:89 msgid "My Profile" @@ -842,8 +840,8 @@ msgid "Play video" msgstr "Play video" #: src/components/settings/views/billing.tsx:33 -msgid "Pro" -msgstr "Pro" +#~ msgid "Pro" +#~ msgstr "Pro" #: src/components/settings/components/settings-header.tsx:22 msgid "Profile" @@ -892,8 +890,8 @@ msgid "Save recordings" msgstr "Save recordings" #: src/components/settings/views/billing.tsx:51 -msgid "Search & ask across all notes in workspace" -msgstr "Search & ask across all notes in workspace" +#~ msgid "Search & ask across all notes in workspace" +#~ msgstr "Search & ask across all notes in workspace" #: src/components/settings/views/team.tsx:204 msgid "Search names or emails" @@ -941,8 +939,8 @@ msgstr "Show notifications when you join a meeting." #~ msgstr "Show notifications when you join a meeting. This is not perfect." #: src/components/settings/views/billing.tsx:53 -msgid "Single sign-on for all users" -msgstr "Single sign-on for all users" +#~ msgid "Single sign-on for all users" +#~ msgstr "Single sign-on for all users" #: src/components/settings/components/main-sidebar.tsx:35 msgid "Sound" @@ -953,16 +951,16 @@ msgstr "Sound" #~ msgstr "Speech-to-Text Model" #: src/components/settings/views/billing.tsx:76 -msgid "Start Annual Plan" -msgstr "Start Annual Plan" +#~ msgid "Start Annual Plan" +#~ msgstr "Start Annual Plan" #: src/components/editor-area/note-header/listen-button.tsx:249 #~ msgid "Start Meeting" #~ msgstr "Start Meeting" #: src/components/settings/views/billing.tsx:75 -msgid "Start Monthly Plan" -msgstr "Start Monthly Plan" +#~ msgid "Start Monthly Plan" +#~ msgstr "Start Monthly Plan" #: src/components/editor-area/note-header/listen-button.tsx:168 msgid "Start recording" @@ -981,14 +979,13 @@ msgid "Summarize meeting" msgstr "Summarize meeting" #: src/components/settings/views/billing.tsx:42 -msgid "Synchronization across multiple devices" -msgstr "Synchronization across multiple devices" +#~ msgid "Synchronization across multiple devices" +#~ msgstr "Synchronization across multiple devices" #: src/components/settings/views/sound.tsx:106 msgid "System Audio Access" msgstr "System Audio Access" -#: src/components/settings/views/billing.tsx:46 #: src/components/settings/components/settings-header.tsx:34 msgid "Team" msgstr "Team" @@ -1006,8 +1003,8 @@ msgid "Templates" msgstr "Templates" #: src/components/settings/views/billing.tsx:113 -msgid "There's a plan for everyone" -msgstr "There's a plan for everyone" +#~ msgid "There's a plan for everyone" +#~ msgstr "There's a plan for everyone" #: src/components/settings/views/profile.tsx:188 msgid "This is a short description of your company." @@ -1066,8 +1063,8 @@ msgid "Upcoming Events" msgstr "Upcoming Events" #: src/components/settings/views/billing.tsx:69 -msgid "Upgrade" -msgstr "Upgrade" +#~ msgid "Upgrade" +#~ msgstr "Upgrade" #: src/components/settings/views/ai.tsx:429 msgid "Use the local Llama 3.2 model for enhanced privacy and offline capability." @@ -1107,12 +1104,12 @@ msgid "We'll only use this to follow up if needed." msgstr "We'll only use this to follow up if needed." #: src/components/settings/views/billing.tsx:25 -msgid "Works both in-person and remotely" -msgstr "Works both in-person and remotely" +#~ msgid "Works both in-person and remotely" +#~ msgstr "Works both in-person and remotely" #: src/components/settings/views/billing.tsx:29 -msgid "Works offline" -msgstr "Works offline" +#~ msgid "Works offline" +#~ msgstr "Works offline" #: src/components/settings/views/general.tsx:219 msgid "You can make Hyprnote takes these words into account when transcribing" diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po index 1eaa07eff..b95b981b9 100644 --- a/apps/desktop/src/locales/ko/messages.po +++ b/apps/desktop/src/locales/ko/messages.po @@ -310,8 +310,8 @@ msgid "and {0} more members" msgstr "" #: src/components/settings/views/billing.tsx:131 -msgid "Annual" -msgstr "" +#~ msgid "Annual" +#~ msgstr "" #: src/components/share-and-permission/publish.tsx:18 msgid "Anyone with the link can view this page" @@ -339,8 +339,8 @@ msgid "Are you sure you want to delete this note?" msgstr "" #: src/components/settings/views/billing.tsx:27 -msgid "Ask questions about past meetings" -msgstr "" +#~ msgid "Ask questions about past meetings" +#~ msgstr "" #: src/components/right-panel/components/chat/chat-message.tsx:18 msgid "Assistant:" @@ -351,12 +351,12 @@ msgid "Back to Settings" msgstr "" #: src/components/settings/components/settings-header.tsx:36 -msgid "Billing" -msgstr "" +#~ msgid "Billing" +#~ msgstr "" #: src/components/settings/views/billing.tsx:104 -msgid "Billing features are currently under development and will be available in a future update." -msgstr "" +#~ msgid "Billing features are currently under development and will be available in a future update." +#~ msgstr "" #: src/components/settings/components/templates-sidebar.tsx:68 msgid "Built-in Templates" @@ -392,12 +392,10 @@ msgid "Choose whether to save your recordings locally." msgstr "" #: src/components/settings/views/billing.tsx:52 -msgid "Collaborate with others in meetings" -msgstr "" +#~ msgid "Collaborate with others in meetings" +#~ msgstr "" #: src/components/settings/views/team.tsx:69 -#: src/components/settings/views/billing.tsx:63 -#: src/components/settings/views/billing.tsx:101 #: src/components/settings/components/extensions-sidebar.tsx:55 msgid "Coming Soon" msgstr "" @@ -453,8 +451,8 @@ msgid "Create Note" msgstr "" #: src/components/settings/views/billing.tsx:66 -msgid "Current Plan" -msgstr "" +#~ msgid "Current Plan" +#~ msgstr "" #: src/components/settings/views/ai.tsx:451 msgid "Custom Endpoint" @@ -568,28 +566,28 @@ msgid "Feedback" msgstr "" #: src/components/settings/views/billing.tsx:47 -msgid "For fast growing teams like energetic startups" -msgstr "" +#~ msgid "For fast growing teams like energetic startups" +#~ msgstr "" #: src/components/settings/views/billing.tsx:34 -msgid "For those who are serious about their performance" -msgstr "" +#~ msgid "For those who are serious about their performance" +#~ msgstr "" #: src/components/settings/views/billing.tsx:21 -msgid "For those who are serious about their privacy" -msgstr "" +#~ msgid "For those who are serious about their privacy" +#~ msgstr "" #: src/components/settings/views/billing.tsx:26 -msgid "Format notes using templates" -msgstr "" +#~ msgid "Format notes using templates" +#~ msgstr "" #: src/components/settings/views/billing.tsx:20 -msgid "Free" -msgstr "" +#~ msgid "Free" +#~ msgstr "" #: src/components/settings/views/billing.tsx:72 -msgid "Free Trial" -msgstr "" +#~ msgid "Free Trial" +#~ msgstr "" #: src/components/settings/views/profile.tsx:119 msgid "Full name" @@ -630,8 +628,8 @@ msgstr "" #~ msgstr "" #: src/components/settings/views/billing.tsx:38 -msgid "Integration with other apps like Notion and Google Calendar" -msgstr "" +#~ msgid "Integration with other apps like Notion and Google Calendar" +#~ msgstr "" #: src/components/share-and-permission/invite-list.tsx:30 msgid "Invite" @@ -666,16 +664,16 @@ msgstr "" #~ msgstr "" #: src/components/settings/views/billing.tsx:200 -msgid "Learn more about our pricing plans" -msgstr "" +#~ msgid "Learn more about our pricing plans" +#~ msgstr "" #: src/components/settings/views/profile.tsx:210 msgid "LinkedIn username" msgstr "" #: src/components/settings/views/billing.tsx:28 -msgid "Live summary of the meeting" -msgstr "" +#~ msgid "Live summary of the meeting" +#~ msgstr "" #: src/components/settings/views/extension.tsx:118 msgid "Loading extension details..." @@ -691,16 +689,16 @@ msgid "Local mode" msgstr "" #: src/components/settings/views/billing.tsx:39 -msgid "Long-term memory for past meetings and attendees" -msgstr "" +#~ msgid "Long-term memory for past meetings and attendees" +#~ msgstr "" #: src/components/share-and-permission/publish.tsx:24 msgid "Make it public" msgstr "" #: src/components/settings/views/billing.tsx:41 -msgid "Meeting note sharing via links" -msgstr "" +#~ msgid "Meeting note sharing via links" +#~ msgstr "" #: src/components/settings/views/team.tsx:145 #: src/components/settings/views/team.tsx:232 @@ -724,12 +722,12 @@ msgid "Model Name" msgstr "" #: src/components/settings/views/billing.tsx:125 -msgid "Monthly" -msgstr "" +#~ msgid "Monthly" +#~ msgstr "" #: src/components/settings/views/billing.tsx:40 -msgid "Much better AI performance" -msgstr "" +#~ msgid "Much better AI performance" +#~ msgstr "" #: src/components/left-sidebar/top-area/settings-button.tsx:89 msgid "My Profile" @@ -842,8 +840,8 @@ msgid "Play video" msgstr "" #: src/components/settings/views/billing.tsx:33 -msgid "Pro" -msgstr "" +#~ msgid "Pro" +#~ msgstr "" #: src/components/settings/components/settings-header.tsx:22 msgid "Profile" @@ -892,8 +890,8 @@ msgid "Save recordings" msgstr "" #: src/components/settings/views/billing.tsx:51 -msgid "Search & ask across all notes in workspace" -msgstr "" +#~ msgid "Search & ask across all notes in workspace" +#~ msgstr "" #: src/components/settings/views/team.tsx:204 msgid "Search names or emails" @@ -941,8 +939,8 @@ msgstr "" #~ msgstr "" #: src/components/settings/views/billing.tsx:53 -msgid "Single sign-on for all users" -msgstr "" +#~ msgid "Single sign-on for all users" +#~ msgstr "" #: src/components/settings/components/main-sidebar.tsx:35 msgid "Sound" @@ -953,16 +951,16 @@ msgstr "" #~ msgstr "" #: src/components/settings/views/billing.tsx:76 -msgid "Start Annual Plan" -msgstr "" +#~ msgid "Start Annual Plan" +#~ msgstr "" #: src/components/editor-area/note-header/listen-button.tsx:249 #~ msgid "Start Meeting" #~ msgstr "" #: src/components/settings/views/billing.tsx:75 -msgid "Start Monthly Plan" -msgstr "" +#~ msgid "Start Monthly Plan" +#~ msgstr "" #: src/components/editor-area/note-header/listen-button.tsx:168 msgid "Start recording" @@ -981,14 +979,13 @@ msgid "Summarize meeting" msgstr "" #: src/components/settings/views/billing.tsx:42 -msgid "Synchronization across multiple devices" -msgstr "" +#~ msgid "Synchronization across multiple devices" +#~ msgstr "" #: src/components/settings/views/sound.tsx:106 msgid "System Audio Access" msgstr "" -#: src/components/settings/views/billing.tsx:46 #: src/components/settings/components/settings-header.tsx:34 msgid "Team" msgstr "" @@ -1006,8 +1003,8 @@ msgid "Templates" msgstr "" #: src/components/settings/views/billing.tsx:113 -msgid "There's a plan for everyone" -msgstr "" +#~ msgid "There's a plan for everyone" +#~ msgstr "" #: src/components/settings/views/profile.tsx:188 msgid "This is a short description of your company." @@ -1066,8 +1063,8 @@ msgid "Upcoming Events" msgstr "" #: src/components/settings/views/billing.tsx:69 -msgid "Upgrade" -msgstr "" +#~ msgid "Upgrade" +#~ msgstr "" #: src/components/settings/views/ai.tsx:429 msgid "Use the local Llama 3.2 model for enhanced privacy and offline capability." @@ -1107,12 +1104,12 @@ msgid "We'll only use this to follow up if needed." msgstr "" #: src/components/settings/views/billing.tsx:25 -msgid "Works both in-person and remotely" -msgstr "" +#~ msgid "Works both in-person and remotely" +#~ msgstr "" #: src/components/settings/views/billing.tsx:29 -msgid "Works offline" -msgstr "" +#~ msgid "Works offline" +#~ msgstr "" #: src/components/settings/views/general.tsx:219 msgid "You can make Hyprnote takes these words into account when transcribing" diff --git a/apps/desktop/src/routes/app.plans.tsx b/apps/desktop/src/routes/app.plans.tsx index 660e2d8b1..dc6566587 100644 --- a/apps/desktop/src/routes/app.plans.tsx +++ b/apps/desktop/src/routes/app.plans.tsx @@ -1,14 +1,25 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { CalendarClock, Check } from "lucide-react"; + +import { useHypr } from "@/contexts"; import { Button } from "@hypr/ui/components/ui/button"; import { ProgressiveBlur } from "@hypr/ui/components/ui/progressive-blur"; import { cn } from "@hypr/ui/lib/utils"; -import { createFileRoute } from "@tanstack/react-router"; -import { Check } from "lucide-react"; export const Route = createFileRoute("/app/plans")({ component: Component, }); function Component() { + const { subscription } = useHypr(); + + const subscriptionInfo = subscription && { + status: subscription.status, + currentPeriodEnd: subscription.current_period_end, + trialEnd: subscription.trial_end, + price: "$9.99/month - Early Bird Pricing", + }; + return (
@@ -34,7 +45,7 @@ function Component() {
@@ -65,6 +77,12 @@ interface PricingCardProps { text: string; onClick: () => void; }; + subscriptionInfo?: { + status: string; + currentPeriodEnd: number; + trialEnd: number | null | undefined; + price: string; + }; } function PricingCard({ @@ -75,10 +93,19 @@ function PricingCard({ features, className, secondaryAction, + subscriptionInfo, }: PricingCardProps) { const isLocalPlan = title === "Local"; const bgImage = isLocalPlan ? "/assets/bg-local-card.jpg" : "/assets/bg-pro-card.jpg"; + const formatDate = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + return (
- {isLocalPlan ? "Free" : "Private Beta"} + {isLocalPlan ? "Free" : "Public Beta"}
@@ -119,6 +146,65 @@ function PricingCard({

{title}

{description}

+ + {!isLocalPlan && subscriptionInfo && ( +
+
+

Plan Status

+ + {subscriptionInfo.status === "active" + ? "Active" + : subscriptionInfo.status === "trialing" + ? "Trial" + : (subscriptionInfo.status + && subscriptionInfo.status.charAt(0).toUpperCase() + subscriptionInfo.status.slice(1)) + || "Unknown"} + +
+ + {subscriptionInfo.price && ( +
+

Price

+ {subscriptionInfo.price} +
+ )} + + {subscriptionInfo.trialEnd && ( +
+

Trial Ends

+
+ + {formatDate(subscriptionInfo.trialEnd)} +
+
+ )} + + {subscriptionInfo.currentPeriodEnd && ( +
+

Next Billing

+
+ + {formatDate(subscriptionInfo.currentPeriodEnd)} +
+
+ )} +
+ )} + + {!isLocalPlan && !subscriptionInfo && ( +
+

$9.99/month - Early Bird Pricing

+
+ )}
{secondaryAction && ( @@ -159,17 +245,18 @@ function PricingCard({ ) : ( - {buttonText} - + )} From 0681e297c65870e1820c3602b97b2c8998371c19 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 14:35:58 -0700 Subject: [PATCH 14/26] init membership plugin --- Cargo.lock | 16 + Cargo.toml | 1 + apps/desktop/package.json | 1 + apps/desktop/src-tauri/Cargo.toml | 1 + .../src-tauri/capabilities/default.json | 1 + apps/desktop/src-tauri/src/lib.rs | 1 + plugins/membership/.gitignore | 17 + plugins/membership/Cargo.toml | 24 ++ plugins/membership/build.rs | 5 + plugins/membership/js/bindings.gen.ts | 85 +++++ plugins/membership/js/index.ts | 1 + plugins/membership/package.json | 11 + .../autogenerated/commands/ping.toml | 13 + .../permissions/autogenerated/reference.md | 43 +++ plugins/membership/permissions/default.toml | 3 + .../permissions/schemas/schema.json | 318 ++++++++++++++++++ plugins/membership/src/commands.rs | 7 + plugins/membership/src/error.rs | 13 + plugins/membership/src/ext.rs | 9 + plugins/membership/src/lib.rs | 55 +++ plugins/membership/tsconfig.json | 5 + pnpm-lock.yaml | 9 + 22 files changed, 639 insertions(+) create mode 100644 plugins/membership/.gitignore create mode 100644 plugins/membership/Cargo.toml create mode 100644 plugins/membership/build.rs create mode 100644 plugins/membership/js/bindings.gen.ts create mode 100644 plugins/membership/js/index.ts create mode 100644 plugins/membership/package.json create mode 100644 plugins/membership/permissions/autogenerated/commands/ping.toml create mode 100644 plugins/membership/permissions/autogenerated/reference.md create mode 100644 plugins/membership/permissions/default.toml create mode 100644 plugins/membership/permissions/schemas/schema.json create mode 100644 plugins/membership/src/commands.rs create mode 100644 plugins/membership/src/error.rs create mode 100644 plugins/membership/src/ext.rs create mode 100644 plugins/membership/src/lib.rs create mode 100644 plugins/membership/tsconfig.json diff --git a/Cargo.lock b/Cargo.lock index 2d25a96d7..188deef58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3340,6 +3340,7 @@ dependencies = [ "tauri-plugin-listener", "tauri-plugin-local-llm", "tauri-plugin-local-stt", + "tauri-plugin-membership", "tauri-plugin-misc", "tauri-plugin-notification", "tauri-plugin-opener", @@ -13208,6 +13209,21 @@ dependencies = [ "ws-utils", ] +[[package]] +name = "tauri-plugin-membership" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "specta", + "specta-typescript", + "strum 0.26.3", + "tauri", + "tauri-plugin", + "tauri-specta", + "thiserror 2.0.12", +] + [[package]] name = "tauri-plugin-misc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 73b43cad8..d6195c5f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ tauri-plugin-flags = { path = "plugins/flags" } tauri-plugin-listener = { path = "plugins/listener" } tauri-plugin-local-llm = { path = "plugins/local-llm" } tauri-plugin-local-stt = { path = "plugins/local-stt" } +tauri-plugin-membership = { path = "plugins/membership" } tauri-plugin-misc = { path = "plugins/misc" } tauri-plugin-notification = { path = "plugins/notification" } tauri-plugin-sfx = { path = "plugins/sfx" } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 8e1d7a502..71de6807f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -35,6 +35,7 @@ "@hypr/plugin-listener": "workspace:^", "@hypr/plugin-local-llm": "workspace:^", "@hypr/plugin-local-stt": "workspace:^", + "@hypr/plugin-membership": "workspace:^", "@hypr/plugin-misc": "workspace:^", "@hypr/plugin-notification": "workspace:^", "@hypr/plugin-sfx": "workspace:^", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 6bc8c06e1..e0d83d547 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ tauri-plugin-flags = { workspace = true } tauri-plugin-listener = { workspace = true } tauri-plugin-local-llm = { workspace = true } tauri-plugin-local-stt = { workspace = true } +tauri-plugin-membership = { workspace = true } tauri-plugin-misc = { workspace = true } tauri-plugin-notification = { workspace = true } tauri-plugin-opener = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 7deca765a..67f5b7a21 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -48,6 +48,7 @@ "deep-link:default", "notification:default", "fs:default", + "membership:default", { "identifier": "opener:allow-open-url", "allow": [ diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index ee8650eda..d806a49b8 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -55,6 +55,7 @@ pub async fn main() { .plugin(tauri_plugin_listener::init()) .plugin(tauri_plugin_sse::init()) .plugin(tauri_plugin_misc::init()) + .plugin(tauri_plugin_membership::init()) .plugin(tauri_plugin_db::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_store::Builder::default().build()) diff --git a/plugins/membership/.gitignore b/plugins/membership/.gitignore new file mode 100644 index 000000000..50d8e32e8 --- /dev/null +++ b/plugins/membership/.gitignore @@ -0,0 +1,17 @@ +/.vs +.DS_Store +.Thumbs.db +*.sublime* +.idea/ +debug.log +package-lock.json +.vscode/settings.json +yarn.lock + +/.tauri +/target +Cargo.lock +node_modules/ + +dist-js +dist diff --git a/plugins/membership/Cargo.toml b/plugins/membership/Cargo.toml new file mode 100644 index 000000000..a05819627 --- /dev/null +++ b/plugins/membership/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "tauri-plugin-membership" +version = "0.1.0" +authors = ["You"] +edition = "2021" +exclude = ["./js"] +links = "tauri-plugin-membership" +description = "" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["test"] } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +serde = { workspace = true } +serde_json = { workspace = true } +specta = { workspace = true } +strum = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } diff --git a/plugins/membership/build.rs b/plugins/membership/build.rs new file mode 100644 index 000000000..029861396 --- /dev/null +++ b/plugins/membership/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["ping"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/membership/js/bindings.gen.ts b/plugins/membership/js/bindings.gen.ts new file mode 100644 index 000000000..d465c99aa --- /dev/null +++ b/plugins/membership/js/bindings.gen.ts @@ -0,0 +1,85 @@ +// @ts-nocheck + + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async ping() : Promise { + return await TAURI_INVOKE("plugin:membership|ping"); +} +} + +/** user-defined events **/ + + + +/** user-defined constants **/ + + + +/** user-defined types **/ + + + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/plugins/membership/js/index.ts b/plugins/membership/js/index.ts new file mode 100644 index 000000000..a96e122f0 --- /dev/null +++ b/plugins/membership/js/index.ts @@ -0,0 +1 @@ +export * from "./bindings.gen"; diff --git a/plugins/membership/package.json b/plugins/membership/package.json new file mode 100644 index 000000000..2db23f1a1 --- /dev/null +++ b/plugins/membership/package.json @@ -0,0 +1,11 @@ +{ + "name": "@hypr/plugin-membership", + "private": true, + "main": "./js/index.ts", + "scripts": { + "codegen": "cargo test -p tauri-plugin-membership" + }, + "dependencies": { + "@tauri-apps/api": "^2.5.0" + } +} diff --git a/plugins/membership/permissions/autogenerated/commands/ping.toml b/plugins/membership/permissions/autogenerated/commands/ping.toml new file mode 100644 index 000000000..1d1358807 --- /dev/null +++ b/plugins/membership/permissions/autogenerated/commands/ping.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-ping" +description = "Enables the ping command without any pre-configured scope." +commands.allow = ["ping"] + +[[permission]] +identifier = "deny-ping" +description = "Denies the ping command without any pre-configured scope." +commands.deny = ["ping"] diff --git a/plugins/membership/permissions/autogenerated/reference.md b/plugins/membership/permissions/autogenerated/reference.md new file mode 100644 index 000000000..8ef00b154 --- /dev/null +++ b/plugins/membership/permissions/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Default permissions for the plugin + +#### This default permission set includes the following: + +- `allow-ping` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`membership:allow-ping` + + + +Enables the ping command without any pre-configured scope. + +
+ +`membership:deny-ping` + + + +Denies the ping command without any pre-configured scope. + +
diff --git a/plugins/membership/permissions/default.toml b/plugins/membership/permissions/default.toml new file mode 100644 index 000000000..cc5a76f22 --- /dev/null +++ b/plugins/membership/permissions/default.toml @@ -0,0 +1,3 @@ +[default] +description = "Default permissions for the plugin" +permissions = ["allow-ping"] diff --git a/plugins/membership/permissions/schemas/schema.json b/plugins/membership/permissions/schemas/schema.json new file mode 100644 index 000000000..ac68e129e --- /dev/null +++ b/plugins/membership/permissions/schemas/schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the ping command without any pre-configured scope.", + "type": "string", + "const": "allow-ping", + "markdownDescription": "Enables the ping command without any pre-configured scope." + }, + { + "description": "Denies the ping command without any pre-configured scope.", + "type": "string", + "const": "deny-ping", + "markdownDescription": "Denies the ping command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`", + "type": "string", + "const": "default", + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/membership/src/commands.rs b/plugins/membership/src/commands.rs new file mode 100644 index 000000000..bd385e771 --- /dev/null +++ b/plugins/membership/src/commands.rs @@ -0,0 +1,7 @@ +use crate::MembershipPluginExt; + +#[tauri::command] +#[specta::specta] +pub(crate) async fn ping(app: tauri::AppHandle) -> Result { + Ok(app.todo().to_string()) +} diff --git a/plugins/membership/src/error.rs b/plugins/membership/src/error.rs new file mode 100644 index 000000000..9dbce37bb --- /dev/null +++ b/plugins/membership/src/error.rs @@ -0,0 +1,13 @@ +use serde::{ser::Serializer, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum Error {} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/membership/src/ext.rs b/plugins/membership/src/ext.rs new file mode 100644 index 000000000..7c1c3ff0e --- /dev/null +++ b/plugins/membership/src/ext.rs @@ -0,0 +1,9 @@ +pub trait MembershipPluginExt { + fn todo(&self) -> &str; +} + +impl> MembershipPluginExt for T { + fn todo(&self) -> &str { + "todo" + } +} diff --git a/plugins/membership/src/lib.rs b/plugins/membership/src/lib.rs new file mode 100644 index 000000000..854bb70a0 --- /dev/null +++ b/plugins/membership/src/lib.rs @@ -0,0 +1,55 @@ +mod commands; +mod error; +mod ext; + +pub use error::*; +pub use ext::*; + +const PLUGIN_NAME: &str = "membership"; + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![ + commands::ping::, + ]) + .error_handling(tauri_specta::ErrorHandlingMode::Throw) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .build() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .header("// @ts-nocheck\n\n") + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + "./js/bindings.gen.ts", + ) + .unwrap() + } + + fn create_app(builder: tauri::Builder) -> tauri::App { + builder + .plugin(init()) + .build(tauri::test::mock_context(tauri::test::noop_assets())) + .unwrap() + } + + #[test] + fn test_membership() { + let _app = create_app(tauri::test::mock_builder()); + } +} diff --git a/plugins/membership/tsconfig.json b/plugins/membership/tsconfig.json new file mode 100644 index 000000000..13b985325 --- /dev/null +++ b/plugins/membership/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../tsconfig.base.json", + "include": ["./js/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0435ee035..516d9f103 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,9 @@ importers: '@hypr/plugin-local-stt': specifier: workspace:^ version: link:../../plugins/local-stt + '@hypr/plugin-membership': + specifier: workspace:^ + version: link:../../plugins/membership '@hypr/plugin-misc': specifier: workspace:^ version: link:../../plugins/misc @@ -1298,6 +1301,12 @@ importers: specifier: ^2.5.0 version: 2.5.0 + plugins/membership: + dependencies: + '@tauri-apps/api': + specifier: ^2.5.0 + version: 2.5.0 + plugins/misc: dependencies: '@tauri-apps/api': From 72ae47cb785a5ace6e9172ff02b32b97f359eeaa Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 14:42:02 -0700 Subject: [PATCH 15/26] add store --- Cargo.lock | 2 ++ plugins/membership/Cargo.toml | 2 ++ plugins/membership/build.rs | 2 +- plugins/membership/js/bindings.gen.ts | 4 +-- .../autogenerated/commands/refresh.toml | 13 +++++++++ .../permissions/autogenerated/reference.md | 28 ++++++++++++++++++- plugins/membership/permissions/default.toml | 2 +- .../permissions/schemas/schema.json | 16 +++++++++-- plugins/membership/src/commands.rs | 4 +-- plugins/membership/src/ext.rs | 13 +++++++-- plugins/membership/src/lib.rs | 5 +++- plugins/membership/src/store.rs | 6 ++++ 12 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 plugins/membership/permissions/autogenerated/commands/refresh.toml create mode 100644 plugins/membership/src/store.rs diff --git a/Cargo.lock b/Cargo.lock index 188deef58..2aff2585b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13220,6 +13220,8 @@ dependencies = [ "strum 0.26.3", "tauri", "tauri-plugin", + "tauri-plugin-store", + "tauri-plugin-store2", "tauri-specta", "thiserror 2.0.12", ] diff --git a/plugins/membership/Cargo.toml b/plugins/membership/Cargo.toml index a05819627..11112e744 100644 --- a/plugins/membership/Cargo.toml +++ b/plugins/membership/Cargo.toml @@ -12,9 +12,11 @@ tauri-plugin = { workspace = true, features = ["build"] } [dev-dependencies] specta-typescript = { workspace = true } +tauri-plugin-store = { workspace = true } [dependencies] tauri = { workspace = true, features = ["test"] } +tauri-plugin-store2 = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } serde = { workspace = true } diff --git a/plugins/membership/build.rs b/plugins/membership/build.rs index 029861396..6b150a304 100644 --- a/plugins/membership/build.rs +++ b/plugins/membership/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["ping"]; +const COMMANDS: &[&str] = &["refresh"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/plugins/membership/js/bindings.gen.ts b/plugins/membership/js/bindings.gen.ts index d465c99aa..2ecc0aa07 100644 --- a/plugins/membership/js/bindings.gen.ts +++ b/plugins/membership/js/bindings.gen.ts @@ -7,8 +7,8 @@ export const commands = { -async ping() : Promise { - return await TAURI_INVOKE("plugin:membership|ping"); +async refresh() : Promise { + return await TAURI_INVOKE("plugin:membership|refresh"); } } diff --git a/plugins/membership/permissions/autogenerated/commands/refresh.toml b/plugins/membership/permissions/autogenerated/commands/refresh.toml new file mode 100644 index 000000000..3ab74367a --- /dev/null +++ b/plugins/membership/permissions/autogenerated/commands/refresh.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-refresh" +description = "Enables the refresh command without any pre-configured scope." +commands.allow = ["refresh"] + +[[permission]] +identifier = "deny-refresh" +description = "Denies the refresh command without any pre-configured scope." +commands.deny = ["refresh"] diff --git a/plugins/membership/permissions/autogenerated/reference.md b/plugins/membership/permissions/autogenerated/reference.md index 8ef00b154..325dc49e5 100644 --- a/plugins/membership/permissions/autogenerated/reference.md +++ b/plugins/membership/permissions/autogenerated/reference.md @@ -4,7 +4,7 @@ Default permissions for the plugin #### This default permission set includes the following: -- `allow-ping` +- `allow-refresh` ## Permission Table @@ -38,6 +38,32 @@ Enables the ping command without any pre-configured scope. Denies the ping command without any pre-configured scope. + + + + + + +`membership:allow-refresh` + + + + +Enables the refresh command without any pre-configured scope. + + + + + + + +`membership:deny-refresh` + + + + +Denies the refresh command without any pre-configured scope. + diff --git a/plugins/membership/permissions/default.toml b/plugins/membership/permissions/default.toml index cc5a76f22..599c00bf7 100644 --- a/plugins/membership/permissions/default.toml +++ b/plugins/membership/permissions/default.toml @@ -1,3 +1,3 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-ping"] +permissions = ["allow-refresh"] diff --git a/plugins/membership/permissions/schemas/schema.json b/plugins/membership/permissions/schemas/schema.json index ac68e129e..087f3b9ba 100644 --- a/plugins/membership/permissions/schemas/schema.json +++ b/plugins/membership/permissions/schemas/schema.json @@ -307,10 +307,22 @@ "markdownDescription": "Denies the ping command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`", + "description": "Enables the refresh command without any pre-configured scope.", + "type": "string", + "const": "allow-refresh", + "markdownDescription": "Enables the refresh command without any pre-configured scope." + }, + { + "description": "Denies the refresh command without any pre-configured scope.", + "type": "string", + "const": "deny-refresh", + "markdownDescription": "Denies the refresh command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`" } ] } diff --git a/plugins/membership/src/commands.rs b/plugins/membership/src/commands.rs index bd385e771..c7e50a724 100644 --- a/plugins/membership/src/commands.rs +++ b/plugins/membership/src/commands.rs @@ -2,6 +2,6 @@ use crate::MembershipPluginExt; #[tauri::command] #[specta::specta] -pub(crate) async fn ping(app: tauri::AppHandle) -> Result { - Ok(app.todo().to_string()) +pub(crate) async fn refresh(app: tauri::AppHandle) -> Result<(), String> { + app.refresh().map_err(|e| e.to_string()) } diff --git a/plugins/membership/src/ext.rs b/plugins/membership/src/ext.rs index 7c1c3ff0e..e01d1a616 100644 --- a/plugins/membership/src/ext.rs +++ b/plugins/membership/src/ext.rs @@ -1,9 +1,16 @@ +use tauri_plugin_store2::StorePluginExt; + pub trait MembershipPluginExt { - fn todo(&self) -> &str; + fn membership_store(&self) -> tauri_plugin_store2::ScopedStore; + fn refresh(&self) -> Result<(), crate::Error>; } impl> MembershipPluginExt for T { - fn todo(&self) -> &str { - "todo" + fn membership_store(&self) -> tauri_plugin_store2::ScopedStore { + self.scoped_store(crate::PLUGIN_NAME).unwrap() + } + + fn refresh(&self) -> Result<(), crate::Error> { + todo!() } } diff --git a/plugins/membership/src/lib.rs b/plugins/membership/src/lib.rs index 854bb70a0..cedcbecad 100644 --- a/plugins/membership/src/lib.rs +++ b/plugins/membership/src/lib.rs @@ -1,9 +1,11 @@ mod commands; mod error; mod ext; +mod store; pub use error::*; pub use ext::*; +pub use store::*; const PLUGIN_NAME: &str = "membership"; @@ -11,7 +13,7 @@ fn make_specta_builder() -> tauri_specta::Builder { tauri_specta::Builder::::new() .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ - commands::ping::, + commands::refresh::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Throw) } @@ -44,6 +46,7 @@ mod test { fn create_app(builder: tauri::Builder) -> tauri::App { builder .plugin(init()) + .plugin(tauri_plugin_store::Builder::default().build()) .build(tauri::test::mock_context(tauri::test::noop_assets())) .unwrap() } diff --git a/plugins/membership/src/store.rs b/plugins/membership/src/store.rs new file mode 100644 index 000000000..f44731f74 --- /dev/null +++ b/plugins/membership/src/store.rs @@ -0,0 +1,6 @@ +use tauri_plugin_store2::ScopedStoreKey; + +#[derive(serde::Deserialize, specta::Type, PartialEq, Eq, Hash, strum::Display)] +pub enum StoreKey {} + +impl ScopedStoreKey for StoreKey {} From 79e5031d347bd87193aff96cd1d78ed938a281de Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 15:53:28 -0700 Subject: [PATCH 16/26] done with membership plugin --- Cargo.lock | 3 + apps/app/server/Cargo.toml | 1 + apps/app/server/openapi.gen.json | 58 ------------------- apps/app/server/src/main.rs | 2 +- apps/app/server/src/native/subscription.rs | 48 ++++----------- apps/desktop/src/contexts/hypr.tsx | 7 ++- .../generated/@tanstack/react-query.gen.ts | 21 +------ packages/client/generated/sdk.gen.ts | 9 +-- packages/client/generated/types.gen.ts | 22 ------- plugins/membership/Cargo.toml | 2 + plugins/membership/build.rs | 2 +- plugins/membership/js/bindings.gen.ts | 8 ++- .../commands/get_subscription.toml | 13 +++++ .../autogenerated/commands/ping.toml | 13 ----- .../permissions/autogenerated/reference.md | 9 +-- plugins/membership/permissions/default.toml | 2 +- .../permissions/schemas/schema.json | 16 ++--- plugins/membership/src/commands.rs | 16 ++++- plugins/membership/src/error.rs | 7 ++- plugins/membership/src/ext.rs | 52 ++++++++++++++++- plugins/membership/src/lib.rs | 1 + plugins/membership/src/store.rs | 4 +- .../autogenerated/commands/ping.toml | 13 ----- .../permissions/autogenerated/reference.md | 26 --------- .../store2/permissions/schemas/schema.json | 12 ---- 25 files changed, 132 insertions(+), 235 deletions(-) create mode 100644 plugins/membership/permissions/autogenerated/commands/get_subscription.toml delete mode 100644 plugins/membership/permissions/autogenerated/commands/ping.toml delete mode 100644 plugins/store2/permissions/autogenerated/commands/ping.toml diff --git a/Cargo.lock b/Cargo.lock index 2aff2585b..dd96d7c38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,7 @@ dependencies = [ "specta-typescript", "strum 0.26.3", "stt", + "tauri-plugin-membership", "thiserror 2.0.12", "tokio", "tower 0.5.2", @@ -13213,6 +13214,8 @@ dependencies = [ name = "tauri-plugin-membership" version = "0.1.0" dependencies = [ + "reqwest 0.12.15", + "schemars", "serde", "serde_json", "specta", diff --git a/apps/app/server/Cargo.toml b/apps/app/server/Cargo.toml index bab75d019..5ed2482d4 100644 --- a/apps/app/server/Cargo.toml +++ b/apps/app/server/Cargo.toml @@ -9,6 +9,7 @@ dotenv = { workspace = true } [dependencies] hypr-auth-interface = { workspace = true } hypr-listener-interface = { workspace = true } +tauri-plugin-membership = { workspace = true } hypr-analytics = { workspace = true } hypr-buffer = { workspace = true } diff --git a/apps/app/server/openapi.gen.json b/apps/app/server/openapi.gen.json index 7938053c1..fb2ec86de 100644 --- a/apps/app/server/openapi.gen.json +++ b/apps/app/server/openapi.gen.json @@ -27,22 +27,6 @@ } } }, - "/api/desktop/subscription": { - "get": { - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Subscription" - } - } - } - } - } - } - }, "/api/web/connect": { "post": { "requestBody": { @@ -1678,48 +1662,6 @@ } ] }, - "Subscription": { - "type": "object", - "required": [ - "current_period_end", - "status" - ], - "properties": { - "current_period_end": { - "type": "integer", - "format": "int64" - }, - "price_id": { - "type": [ - "string", - "null" - ] - }, - "status": { - "$ref": "#/components/schemas/SubscriptionStatus" - }, - "trial_end": { - "type": [ - "integer", - "null" - ], - "format": "int64" - } - } - }, - "SubscriptionStatus": { - "type": "string", - "enum": [ - "active", - "canceled", - "incomplete", - "incomplete_expired", - "past_due", - "paused", - "trialing", - "unpaid" - ] - }, "TranscriptChunk": { "type": "object", "required": [ diff --git a/apps/app/server/src/main.rs b/apps/app/server/src/main.rs index 535cb047a..4bf4cc7e8 100644 --- a/apps/app/server/src/main.rs +++ b/apps/app/server/src/main.rs @@ -192,7 +192,7 @@ fn main() { "/user/integrations", api_get(native::user::list_integrations), ) - .api_route("/subscription", api_get(native::subscription::handler)) + .route("/subscription", get(native::subscription::handler)) .route("/listen/realtime", get(native::listen::realtime::handler)) .layer( tower::builder::ServiceBuilder::new() diff --git a/apps/app/server/src/native/subscription.rs b/apps/app/server/src/native/subscription.rs index f92d964a4..2419e8519 100644 --- a/apps/app/server/src/native/subscription.rs +++ b/apps/app/server/src/native/subscription.rs @@ -1,10 +1,20 @@ use axum::{http::StatusCode, Extension, Json}; +use tauri_plugin_membership::{Subscription, SubscriptionStatus}; pub async fn handler( Extension(billing): Extension, ) -> Result, StatusCode> { let subscription = billing.stripe_subscription.map(|s| Subscription { - status: s.status.into(), + status: match s.status { + stripe::SubscriptionStatus::Active => SubscriptionStatus::Active, + stripe::SubscriptionStatus::Canceled => SubscriptionStatus::Canceled, + stripe::SubscriptionStatus::Incomplete => SubscriptionStatus::Incomplete, + stripe::SubscriptionStatus::IncompleteExpired => SubscriptionStatus::IncompleteExpired, + stripe::SubscriptionStatus::PastDue => SubscriptionStatus::PastDue, + stripe::SubscriptionStatus::Paused => SubscriptionStatus::Paused, + stripe::SubscriptionStatus::Trialing => SubscriptionStatus::Trialing, + stripe::SubscriptionStatus::Unpaid => SubscriptionStatus::Unpaid, + }, current_period_end: s.current_period_end, trial_end: s.trial_end, price_id: s @@ -21,39 +31,3 @@ pub async fn handler( Err(StatusCode::NOT_FOUND) } } - -#[derive(Debug, serde::Serialize, schemars::JsonSchema)] -pub struct Subscription { - pub status: SubscriptionStatus, - pub current_period_end: i64, - pub trial_end: Option, - pub price_id: Option, -} - -#[derive(Debug, serde::Serialize, schemars::JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SubscriptionStatus { - Active, - Canceled, - Incomplete, - IncompleteExpired, - PastDue, - Paused, - Trialing, - Unpaid, -} - -impl From for SubscriptionStatus { - fn from(status: stripe::SubscriptionStatus) -> Self { - match status { - stripe::SubscriptionStatus::Active => SubscriptionStatus::Active, - stripe::SubscriptionStatus::Canceled => SubscriptionStatus::Canceled, - stripe::SubscriptionStatus::Incomplete => SubscriptionStatus::Incomplete, - stripe::SubscriptionStatus::IncompleteExpired => SubscriptionStatus::IncompleteExpired, - stripe::SubscriptionStatus::PastDue => SubscriptionStatus::PastDue, - stripe::SubscriptionStatus::Paused => SubscriptionStatus::Paused, - stripe::SubscriptionStatus::Trialing => SubscriptionStatus::Trialing, - stripe::SubscriptionStatus::Unpaid => SubscriptionStatus::Unpaid, - } - } -} diff --git a/apps/desktop/src/contexts/hypr.tsx b/apps/desktop/src/contexts/hypr.tsx index 2c63e6a26..50ac78646 100644 --- a/apps/desktop/src/contexts/hypr.tsx +++ b/apps/desktop/src/contexts/hypr.tsx @@ -1,9 +1,9 @@ import { useQueries } from "@tanstack/react-query"; import { createContext, useContext } from "react"; -import { getApiDesktopSubscriptionOptions, type Subscription } from "@/client"; import { commands as authCommands } from "@hypr/plugin-auth"; import { commands as dbCommands } from "@hypr/plugin-db"; +import { commands as membershipCommands, type Subscription } from "@hypr/plugin-membership"; export interface HyprContext { userId: string; @@ -24,7 +24,10 @@ export function HyprProvider({ children }: { children: React.ReactNode }) { queryKey: ["onboarding-session-id"], queryFn: () => dbCommands.onboardingSessionId(), }, - getApiDesktopSubscriptionOptions(), + { + queryKey: ["subscription"], + queryFn: () => membershipCommands.refresh(), + }, ], }); diff --git a/packages/client/generated/@tanstack/react-query.gen.ts b/packages/client/generated/@tanstack/react-query.gen.ts index 5db4dcced..c6e1ebca9 100644 --- a/packages/client/generated/@tanstack/react-query.gen.ts +++ b/packages/client/generated/@tanstack/react-query.gen.ts @@ -1,8 +1,8 @@ // This file is auto-generated by @hey-api/openapi-ts -import { type Options, getHealth, getApiDesktopUserIntegrations, getApiDesktopSubscription, postApiWebConnect, getApiWebCheckout, getApiWebSessionById, postApiWebIntegrationConnection, postChatCompletions } from '../sdk.gen'; +import { type Options, getHealth, getApiDesktopUserIntegrations, postApiWebConnect, getApiWebCheckout, getApiWebSessionById, postApiWebIntegrationConnection, postChatCompletions } from '../sdk.gen'; import { queryOptions, type UseMutationOptions, type DefaultError } from '@tanstack/react-query'; -import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopSubscriptionData, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from '../types.gen'; +import type { GetHealthData, GetApiDesktopUserIntegrationsData, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from '../types.gen'; import { client as _heyApiClient } from '../client.gen'; export type QueryKey = [ @@ -70,23 +70,6 @@ export const getApiDesktopUserIntegrationsOptions = (options?: Options) => createQueryKey('getApiDesktopSubscription', options); - -export const getApiDesktopSubscriptionOptions = (options?: Options) => { - return queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getApiDesktopSubscription({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getApiDesktopSubscriptionQueryKey(options) - }); -}; - export const postApiWebConnectQueryKey = (options: Options) => createQueryKey('postApiWebConnect', options); export const postApiWebConnectOptions = (options: Options) => { diff --git a/packages/client/generated/sdk.gen.ts b/packages/client/generated/sdk.gen.ts index 5ea8e8990..d1220f6c6 100644 --- a/packages/client/generated/sdk.gen.ts +++ b/packages/client/generated/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopUserIntegrationsResponse, GetApiDesktopSubscriptionData, GetApiDesktopSubscriptionResponse, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, GetApiWebSessionByIdResponse, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from './types.gen'; +import type { GetHealthData, GetApiDesktopUserIntegrationsData, GetApiDesktopUserIntegrationsResponse, PostApiWebConnectData, PostApiWebConnectResponse, GetApiWebCheckoutData, GetApiWebSessionByIdData, GetApiWebSessionByIdResponse, PostApiWebIntegrationConnectionData, PostApiWebIntegrationConnectionResponse, PostChatCompletionsData } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -32,13 +32,6 @@ export const getApiDesktopUserIntegrations = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ - url: '/api/desktop/subscription', - ...options - }); -}; - export const postApiWebConnect = (options: Options) => { return (options.client ?? _heyApiClient).post({ url: '/api/web/connect', diff --git a/packages/client/generated/types.gen.ts b/packages/client/generated/types.gen.ts index 81154f05c..b66213b6c 100644 --- a/packages/client/generated/types.gen.ts +++ b/packages/client/generated/types.gen.ts @@ -577,15 +577,6 @@ export type Session = { export type Stop = string | Array; -export type Subscription = { - current_period_end: number; - price_id?: string | null; - status: SubscriptionStatus; - trial_end?: number | null; -}; - -export type SubscriptionStatus = 'active' | 'canceled' | 'incomplete' | 'incomplete_expired' | 'past_due' | 'paused' | 'trialing' | 'unpaid'; - export type TranscriptChunk = { confidence?: number | null; end: number; @@ -613,19 +604,6 @@ export type GetApiDesktopUserIntegrationsResponses = { export type GetApiDesktopUserIntegrationsResponse = GetApiDesktopUserIntegrationsResponses[keyof GetApiDesktopUserIntegrationsResponses]; -export type GetApiDesktopSubscriptionData = { - body?: never; - path?: never; - query?: never; - url: '/api/desktop/subscription'; -}; - -export type GetApiDesktopSubscriptionResponses = { - 200: Subscription; -}; - -export type GetApiDesktopSubscriptionResponse = GetApiDesktopSubscriptionResponses[keyof GetApiDesktopSubscriptionResponses]; - export type PostApiWebConnectData = { body: RequestParams; path?: never; diff --git a/plugins/membership/Cargo.toml b/plugins/membership/Cargo.toml index 11112e744..24bf7c353 100644 --- a/plugins/membership/Cargo.toml +++ b/plugins/membership/Cargo.toml @@ -19,6 +19,8 @@ tauri = { workspace = true, features = ["test"] } tauri-plugin-store2 = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } +reqwest = { workspace = true, features = ["json"] } +schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } specta = { workspace = true } diff --git a/plugins/membership/build.rs b/plugins/membership/build.rs index 6b150a304..cdce1433d 100644 --- a/plugins/membership/build.rs +++ b/plugins/membership/build.rs @@ -1,4 +1,4 @@ -const COMMANDS: &[&str] = &["refresh"]; +const COMMANDS: &[&str] = &["refresh", "get_subscription"]; fn main() { tauri_plugin::Builder::new(COMMANDS).build(); diff --git a/plugins/membership/js/bindings.gen.ts b/plugins/membership/js/bindings.gen.ts index 2ecc0aa07..943550aa7 100644 --- a/plugins/membership/js/bindings.gen.ts +++ b/plugins/membership/js/bindings.gen.ts @@ -7,8 +7,11 @@ export const commands = { -async refresh() : Promise { +async refresh() : Promise { return await TAURI_INVOKE("plugin:membership|refresh"); +}, +async getSubscription() : Promise { + return await TAURI_INVOKE("plugin:membership|get_subscription"); } } @@ -22,7 +25,8 @@ async refresh() : Promise { /** user-defined types **/ - +export type Subscription = { status: SubscriptionStatus; current_period_end: number; trial_end: number | null; price_id: string | null } +export type SubscriptionStatus = "active" | "canceled" | "incomplete" | "incomplete_expired" | "past_due" | "paused" | "trialing" | "unpaid" /** tauri-specta globals **/ diff --git a/plugins/membership/permissions/autogenerated/commands/get_subscription.toml b/plugins/membership/permissions/autogenerated/commands/get_subscription.toml new file mode 100644 index 000000000..d8621fba0 --- /dev/null +++ b/plugins/membership/permissions/autogenerated/commands/get_subscription.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-get-subscription" +description = "Enables the get_subscription command without any pre-configured scope." +commands.allow = ["get_subscription"] + +[[permission]] +identifier = "deny-get-subscription" +description = "Denies the get_subscription command without any pre-configured scope." +commands.deny = ["get_subscription"] diff --git a/plugins/membership/permissions/autogenerated/commands/ping.toml b/plugins/membership/permissions/autogenerated/commands/ping.toml deleted file mode 100644 index 1d1358807..000000000 --- a/plugins/membership/permissions/autogenerated/commands/ping.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-ping" -description = "Enables the ping command without any pre-configured scope." -commands.allow = ["ping"] - -[[permission]] -identifier = "deny-ping" -description = "Denies the ping command without any pre-configured scope." -commands.deny = ["ping"] diff --git a/plugins/membership/permissions/autogenerated/reference.md b/plugins/membership/permissions/autogenerated/reference.md index 325dc49e5..d1ae86b7b 100644 --- a/plugins/membership/permissions/autogenerated/reference.md +++ b/plugins/membership/permissions/autogenerated/reference.md @@ -5,6 +5,7 @@ Default permissions for the plugin #### This default permission set includes the following: - `allow-refresh` +- `allow-get-subscription` ## Permission Table @@ -18,12 +19,12 @@ Default permissions for the plugin -`membership:allow-ping` +`membership:allow-get-subscription` -Enables the ping command without any pre-configured scope. +Enables the get_subscription command without any pre-configured scope. @@ -31,12 +32,12 @@ Enables the ping command without any pre-configured scope. -`membership:deny-ping` +`membership:deny-get-subscription` -Denies the ping command without any pre-configured scope. +Denies the get_subscription command without any pre-configured scope. diff --git a/plugins/membership/permissions/default.toml b/plugins/membership/permissions/default.toml index 599c00bf7..75e459a13 100644 --- a/plugins/membership/permissions/default.toml +++ b/plugins/membership/permissions/default.toml @@ -1,3 +1,3 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-refresh"] +permissions = ["allow-refresh", "allow-get-subscription"] diff --git a/plugins/membership/permissions/schemas/schema.json b/plugins/membership/permissions/schemas/schema.json index 087f3b9ba..88a70047c 100644 --- a/plugins/membership/permissions/schemas/schema.json +++ b/plugins/membership/permissions/schemas/schema.json @@ -295,16 +295,16 @@ "type": "string", "oneOf": [ { - "description": "Enables the ping command without any pre-configured scope.", + "description": "Enables the get_subscription command without any pre-configured scope.", "type": "string", - "const": "allow-ping", - "markdownDescription": "Enables the ping command without any pre-configured scope." + "const": "allow-get-subscription", + "markdownDescription": "Enables the get_subscription command without any pre-configured scope." }, { - "description": "Denies the ping command without any pre-configured scope.", + "description": "Denies the get_subscription command without any pre-configured scope.", "type": "string", - "const": "deny-ping", - "markdownDescription": "Denies the ping command without any pre-configured scope." + "const": "deny-get-subscription", + "markdownDescription": "Denies the get_subscription command without any pre-configured scope." }, { "description": "Enables the refresh command without any pre-configured scope.", @@ -319,10 +319,10 @@ "markdownDescription": "Denies the refresh command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`\n- `allow-get-subscription`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-refresh`\n- `allow-get-subscription`" } ] } diff --git a/plugins/membership/src/commands.rs b/plugins/membership/src/commands.rs index c7e50a724..c570fb611 100644 --- a/plugins/membership/src/commands.rs +++ b/plugins/membership/src/commands.rs @@ -1,7 +1,17 @@ -use crate::MembershipPluginExt; +use crate::{MembershipPluginExt, Subscription}; #[tauri::command] #[specta::specta] -pub(crate) async fn refresh(app: tauri::AppHandle) -> Result<(), String> { - app.refresh().map_err(|e| e.to_string()) +pub(crate) async fn refresh( + app: tauri::AppHandle, +) -> Result { + app.refresh().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +#[specta::specta] +pub(crate) async fn get_subscription( + app: tauri::AppHandle, +) -> Result, String> { + app.get_subscription().await.map_err(|e| e.to_string()) } diff --git a/plugins/membership/src/error.rs b/plugins/membership/src/error.rs index 9dbce37bb..42c1a7515 100644 --- a/plugins/membership/src/error.rs +++ b/plugins/membership/src/error.rs @@ -1,7 +1,12 @@ use serde::{ser::Serializer, Serialize}; #[derive(Debug, thiserror::Error)] -pub enum Error {} +pub enum Error { + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + StoreError(#[from] tauri_plugin_store2::Error), +} impl Serialize for Error { fn serialize(&self, serializer: S) -> std::result::Result diff --git a/plugins/membership/src/ext.rs b/plugins/membership/src/ext.rs index e01d1a616..34e55b08f 100644 --- a/plugins/membership/src/ext.rs +++ b/plugins/membership/src/ext.rs @@ -1,8 +1,10 @@ +use std::future::Future; use tauri_plugin_store2::StorePluginExt; pub trait MembershipPluginExt { fn membership_store(&self) -> tauri_plugin_store2::ScopedStore; - fn refresh(&self) -> Result<(), crate::Error>; + fn get_subscription(&self) -> impl Future, crate::Error>>; + fn refresh(&self) -> impl Future>; } impl> MembershipPluginExt for T { @@ -10,7 +12,51 @@ impl> MembershipPluginExt for T { self.scoped_store(crate::PLUGIN_NAME).unwrap() } - fn refresh(&self) -> Result<(), crate::Error> { - todo!() + async fn get_subscription(&self) -> Result, crate::Error> { + let data = self + .membership_store() + .get::(crate::StoreKey::Subscription)?; + + Ok(data) + } + + async fn refresh(&self) -> Result { + let url = if cfg!(debug_assertions) { + "http://localhost:1234/api/desktop/subscription" + } else { + "https://app.hypr.com/api/desktop/subscription" + }; + + let resp = reqwest::get(url).await?; + let data: Subscription = resp.json().await?; + + self.membership_store() + .set(crate::StoreKey::Subscription, data.clone())?; + Ok(data) } } + +#[derive( + Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, specta::Type, +)] +pub struct Subscription { + pub status: SubscriptionStatus, + pub current_period_end: i64, + pub trial_end: Option, + pub price_id: Option, +} + +#[derive( + Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, specta::Type, +)] +#[serde(rename_all = "snake_case")] +pub enum SubscriptionStatus { + Active, + Canceled, + Incomplete, + IncompleteExpired, + PastDue, + Paused, + Trialing, + Unpaid, +} diff --git a/plugins/membership/src/lib.rs b/plugins/membership/src/lib.rs index cedcbecad..219c8ae95 100644 --- a/plugins/membership/src/lib.rs +++ b/plugins/membership/src/lib.rs @@ -14,6 +14,7 @@ fn make_specta_builder() -> tauri_specta::Builder { .plugin_name(PLUGIN_NAME) .commands(tauri_specta::collect_commands![ commands::refresh::, + commands::get_subscription::, ]) .error_handling(tauri_specta::ErrorHandlingMode::Throw) } diff --git a/plugins/membership/src/store.rs b/plugins/membership/src/store.rs index f44731f74..b725f6b98 100644 --- a/plugins/membership/src/store.rs +++ b/plugins/membership/src/store.rs @@ -1,6 +1,8 @@ use tauri_plugin_store2::ScopedStoreKey; #[derive(serde::Deserialize, specta::Type, PartialEq, Eq, Hash, strum::Display)] -pub enum StoreKey {} +pub enum StoreKey { + Subscription, +} impl ScopedStoreKey for StoreKey {} diff --git a/plugins/store2/permissions/autogenerated/commands/ping.toml b/plugins/store2/permissions/autogenerated/commands/ping.toml deleted file mode 100644 index 1d1358807..000000000 --- a/plugins/store2/permissions/autogenerated/commands/ping.toml +++ /dev/null @@ -1,13 +0,0 @@ -# Automatically generated - DO NOT EDIT! - -"$schema" = "../../schemas/schema.json" - -[[permission]] -identifier = "allow-ping" -description = "Enables the ping command without any pre-configured scope." -commands.allow = ["ping"] - -[[permission]] -identifier = "deny-ping" -description = "Denies the ping command without any pre-configured scope." -commands.deny = ["ping"] diff --git a/plugins/store2/permissions/autogenerated/reference.md b/plugins/store2/permissions/autogenerated/reference.md index fc1c01fff..647806d5e 100644 --- a/plugins/store2/permissions/autogenerated/reference.md +++ b/plugins/store2/permissions/autogenerated/reference.md @@ -96,32 +96,6 @@ Denies the get_str command without any pre-configured scope. -`store2:allow-ping` - - - - -Enables the ping command without any pre-configured scope. - - - - - - - -`store2:deny-ping` - - - - -Denies the ping command without any pre-configured scope. - - - - - - - `store2:allow-set-bool` diff --git a/plugins/store2/permissions/schemas/schema.json b/plugins/store2/permissions/schemas/schema.json index 8ebfaf46a..9c2e9b255 100644 --- a/plugins/store2/permissions/schemas/schema.json +++ b/plugins/store2/permissions/schemas/schema.json @@ -330,18 +330,6 @@ "const": "deny-get-str", "markdownDescription": "Denies the get_str command without any pre-configured scope." }, - { - "description": "Enables the ping command without any pre-configured scope.", - "type": "string", - "const": "allow-ping", - "markdownDescription": "Enables the ping command without any pre-configured scope." - }, - { - "description": "Denies the ping command without any pre-configured scope.", - "type": "string", - "const": "deny-ping", - "markdownDescription": "Denies the ping command without any pre-configured scope." - }, { "description": "Enables the set_bool command without any pre-configured scope.", "type": "string", From c1f1df2c53235ed94d7140410523d807650b1a57 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 16:10:59 -0700 Subject: [PATCH 17/26] fix permission stuffs --- plugins/store2/permissions/autogenerated/reference.md | 7 ++++++- plugins/store2/permissions/default.toml | 9 ++++++++- plugins/store2/permissions/schemas/schema.json | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plugins/store2/permissions/autogenerated/reference.md b/plugins/store2/permissions/autogenerated/reference.md index 647806d5e..af119546b 100644 --- a/plugins/store2/permissions/autogenerated/reference.md +++ b/plugins/store2/permissions/autogenerated/reference.md @@ -4,7 +4,12 @@ Default permissions for the plugin #### This default permission set includes the following: -- `allow-ping` +- `allow-get-str` +- `allow-set-str` +- `allow-get-bool` +- `allow-set-bool` +- `allow-get-number` +- `allow-set-number` ## Permission Table diff --git a/plugins/store2/permissions/default.toml b/plugins/store2/permissions/default.toml index cc5a76f22..3bd8d29bb 100644 --- a/plugins/store2/permissions/default.toml +++ b/plugins/store2/permissions/default.toml @@ -1,3 +1,10 @@ [default] description = "Default permissions for the plugin" -permissions = ["allow-ping"] +permissions = [ + "allow-get-str", + "allow-set-str", + "allow-get-bool", + "allow-set-bool", + "allow-get-number", + "allow-set-number", +] diff --git a/plugins/store2/permissions/schemas/schema.json b/plugins/store2/permissions/schemas/schema.json index 9c2e9b255..5edd10a05 100644 --- a/plugins/store2/permissions/schemas/schema.json +++ b/plugins/store2/permissions/schemas/schema.json @@ -367,10 +367,10 @@ "markdownDescription": "Denies the set_str command without any pre-configured scope." }, { - "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`", + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-get-str`\n- `allow-set-str`\n- `allow-get-bool`\n- `allow-set-bool`\n- `allow-get-number`\n- `allow-set-number`", "type": "string", "const": "default", - "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-ping`" + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-get-str`\n- `allow-set-str`\n- `allow-get-bool`\n- `allow-set-bool`\n- `allow-get-number`\n- `allow-set-number`" } ] } From f098b88f54063c489139ce3905c50988ff6e3d05 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 19:30:00 -0700 Subject: [PATCH 18/26] ui update --- .../left-sidebar/top-area/settings-button.tsx | 78 ++-- apps/desktop/src/contexts/hypr.tsx | 12 +- apps/desktop/src/routes/__root.tsx | 4 +- apps/desktop/src/routes/app.plans.tsx | 376 +++++++++++------- 4 files changed, 298 insertions(+), 172 deletions(-) diff --git a/apps/desktop/src/components/left-sidebar/top-area/settings-button.tsx b/apps/desktop/src/components/left-sidebar/top-area/settings-button.tsx index fafa8004c..657816761 100644 --- a/apps/desktop/src/components/left-sidebar/top-area/settings-button.tsx +++ b/apps/desktop/src/components/left-sidebar/top-area/settings-button.tsx @@ -1,6 +1,6 @@ import { Trans } from "@lingui/react/macro"; import { getName, getVersion } from "@tauri-apps/api/app"; -import { CogIcon, CpuIcon } from "lucide-react"; +import { CogIcon, CpuIcon, TrainFrontIcon } from "lucide-react"; import { useState } from "react"; import Shortcut from "@/components/shortcut"; @@ -18,7 +18,7 @@ import { useQuery } from "@tanstack/react-query"; export function SettingsButton() { const [open, setOpen] = useState(false); - const { userId } = useHypr(); + const { userId, isPro } = useHypr(); const versionQuery = useQuery({ queryKey: ["appVersion"], @@ -52,27 +52,7 @@ export function SettingsButton() { -
-
-
-
- -
-
- Local mode -
-
- Privacy-focused AI -
-
-
-
+ {isPro ? : }
); } + +function ProMode({ onClick }: { onClick: () => void }) { + return ( +
+
+
+
+ +
+
+ Pro mode +
+
+ For professional use +
+
+
+
+ ); +} + +function LocalMode({ onClick }: { onClick: () => void }) { + return ( +
+
+
+
+ +
+
+ Local mode +
+
+ Privacy-focused AI +
+
+
+
+ ); +} diff --git a/apps/desktop/src/contexts/hypr.tsx b/apps/desktop/src/contexts/hypr.tsx index 50ac78646..7f027f30e 100644 --- a/apps/desktop/src/contexts/hypr.tsx +++ b/apps/desktop/src/contexts/hypr.tsx @@ -9,6 +9,7 @@ export interface HyprContext { userId: string; onboardingSessionId: string; subscription?: Subscription; + isPro: boolean; } const HyprContext = createContext(null); @@ -44,10 +45,15 @@ export function HyprProvider({ children }: { children: React.ReactNode }) { return null; } + const value = { + userId: userId.data, + onboardingSessionId: onboardingSessionId.data, + subscription: subscription.data, + isPro: subscription.data?.status === "active" || subscription.data?.status === "trialing", + }; + return ( - + {children} ); diff --git a/apps/desktop/src/routes/__root.tsx b/apps/desktop/src/routes/__root.tsx index 45fb285c6..a215b51e8 100644 --- a/apps/desktop/src/routes/__root.tsx +++ b/apps/desktop/src/routes/__root.tsx @@ -46,9 +46,7 @@ function Component() { }, [navigate]); useEffect(() => { - scan({ - enabled: true, - }); + scan({ enabled: false }); }, []); return ( diff --git a/apps/desktop/src/routes/app.plans.tsx b/apps/desktop/src/routes/app.plans.tsx index dc6566587..741e258dc 100644 --- a/apps/desktop/src/routes/app.plans.tsx +++ b/apps/desktop/src/routes/app.plans.tsx @@ -1,7 +1,9 @@ import { createFileRoute } from "@tanstack/react-router"; -import { CalendarClock, Check } from "lucide-react"; +import { format } from "date-fns"; +import { Check } from "lucide-react"; import { useHypr } from "@/contexts"; +import { type Subscription } from "@hypr/plugin-membership"; import { Button } from "@hypr/ui/components/ui/button"; import { ProgressiveBlur } from "@hypr/ui/components/ui/progressive-blur"; import { cn } from "@hypr/ui/lib/utils"; @@ -13,57 +15,20 @@ export const Route = createFileRoute("/app/plans")({ function Component() { const { subscription } = useHypr(); - const subscriptionInfo = subscription && { - status: subscription.status, - currentPeriodEnd: subscription.current_period_end, - trialEnd: subscription.trial_end, - price: "$9.99/month - Early Bird Pricing", - }; + if (!subscription) { + return ; + } - return ( -
-
-
- + const { status, trial_end } = subscription; + const isActive = ["active", "trialing"].includes(status); - -
-
-
- ); + if (!isActive) { + return ; + } + + return trial_end + ? + : ; } interface PricingCardProps { @@ -77,12 +42,8 @@ interface PricingCardProps { text: string; onClick: () => void; }; - subscriptionInfo?: { - status: string; - currentPeriodEnd: number; - trialEnd: number | null | undefined; - price: string; - }; + isActive?: boolean; + subscriptionInfo?: React.ReactNode; } function PricingCard({ @@ -93,24 +54,18 @@ function PricingCard({ features, className, secondaryAction, + isActive = false, subscriptionInfo, }: PricingCardProps) { const isLocalPlan = title === "Local"; const bgImage = isLocalPlan ? "/assets/bg-local-card.jpg" : "/assets/bg-pro-card.jpg"; - const formatDate = (timestamp: number) => { - return new Date(timestamp * 1000).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); - }; - return (
{isLocalPlan ? "Free" : "Public Beta"}
+ + {isActive && ( +
+ Active +
+ )}
-
-

{title}

-

{description}

- - {!isLocalPlan && subscriptionInfo && ( -
-
-

Plan Status

- - {subscriptionInfo.status === "active" - ? "Active" - : subscriptionInfo.status === "trialing" - ? "Trial" - : (subscriptionInfo.status - && subscriptionInfo.status.charAt(0).toUpperCase() + subscriptionInfo.status.slice(1)) - || "Unknown"} - -
- - {subscriptionInfo.price && ( -
-

Price

- {subscriptionInfo.price} -
- )} - - {subscriptionInfo.trialEnd && ( -
-

Trial Ends

-
- - {formatDate(subscriptionInfo.trialEnd)} -
-
- )} - - {subscriptionInfo.currentPeriodEnd && ( -
-

Next Billing

-
- - {formatDate(subscriptionInfo.currentPeriodEnd)} -
-
- )} -
- )} - - {!isLocalPlan && !subscriptionInfo && ( -
-

$9.99/month - Early Bird Pricing

-
- )} +
+

{title}

+

{description}

+ {subscriptionInfo}
{secondaryAction && ( )} -
+
{features.map((feature, i) => (
@@ -235,30 +138,217 @@ function PricingCard({ variant={buttonVariant} size="md" className={cn( - "w-full py-4 text-md font-medium rounded-xl transition-all duration-300 relative z-10 text-center", + "w-full py-3 text-md font-medium rounded-xl transition-all duration-300 relative z-10 text-center", buttonVariant === "default" ? "bg-blue-500 hover:bg-blue-600 shadow-md hover:shadow-lg text-white" : "bg-white/20 hover:bg-white/30 hover:text-white text-white border-white/40", )} + disabled={isActive} > - {buttonText} + {isActive ? "Current Plan" : buttonText} ) : ( - + <> + {!isActive + ? ( +
+ + Upgrade to Pro + +
+

+ 7-day free trial. No credit card required. +

+
+
+ ) + : ( + + )} + )}
); } + +function RenderActiveWithoutTrial({ subscription }: { subscription: Subscription }) { + const nextBillingDate = subscription.current_period_end + ? new Date(subscription.current_period_end * 1000) + : null; + + return ( +
+
+
+ + + +

Next billing: {format(nextBillingDate, "MMMM dd, yyyy")}

+
+ )} + /> +
+ +
+ ); +} + +function RenderActiveWithTrial({ subscription }: { subscription: Subscription }) { + const trialEndDate = subscription.trial_end + ? new Date(subscription.trial_end * 1000) + : null; + + return ( +
+
+
+ + + +

Trial ends: {format(trialEndDate, "MMMM dd, yyyy")}

+
+ )} + /> +
+ +
+ ); +} + +function RenderInactive() { + return ( +
+
+
+ + + +
+
+
+ ); +} From 80d604d33f0887b0adced568d209ff5b1b4031a1 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 19:30:53 -0700 Subject: [PATCH 19/26] i18n --- apps/desktop/src/locales/en/messages.po | 10 +++++++--- apps/desktop/src/locales/ko/messages.po | 10 +++++++--- apps/docs/data/i18n.json | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po index c0514de8f..8ea151f62 100644 --- a/apps/desktop/src/locales/en/messages.po +++ b/apps/desktop/src/locales/en/messages.po @@ -684,7 +684,7 @@ msgstr "Loading extension details..." msgid "Loading..." msgstr "Loading..." -#: src/components/left-sidebar/top-area/settings-button.tsx:68 +#: src/components/left-sidebar/top-area/settings-button.tsx:121 msgid "Local mode" msgstr "Local mode" @@ -729,7 +729,7 @@ msgstr "Model Name" #~ msgid "Much better AI performance" #~ msgstr "Much better AI performance" -#: src/components/left-sidebar/top-area/settings-button.tsx:89 +#: src/components/left-sidebar/top-area/settings-button.tsx:69 msgid "My Profile" msgstr "My Profile" @@ -843,6 +843,10 @@ msgstr "Play video" #~ msgid "Pro" #~ msgstr "Pro" +#: src/components/left-sidebar/top-area/settings-button.tsx:95 +msgid "Pro mode" +msgstr "Pro mode" + #: src/components/settings/components/settings-header.tsx:22 msgid "Profile" msgstr "Profile" @@ -918,7 +922,7 @@ msgstr "Select Calendars" msgid "Send invite" msgstr "Send invite" -#: src/components/left-sidebar/top-area/settings-button.tsx:82 +#: src/components/left-sidebar/top-area/settings-button.tsx:62 msgid "Settings" msgstr "Settings" diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po index b95b981b9..80bfb75d7 100644 --- a/apps/desktop/src/locales/ko/messages.po +++ b/apps/desktop/src/locales/ko/messages.po @@ -684,7 +684,7 @@ msgstr "" msgid "Loading..." msgstr "" -#: src/components/left-sidebar/top-area/settings-button.tsx:68 +#: src/components/left-sidebar/top-area/settings-button.tsx:121 msgid "Local mode" msgstr "" @@ -729,7 +729,7 @@ msgstr "" #~ msgid "Much better AI performance" #~ msgstr "" -#: src/components/left-sidebar/top-area/settings-button.tsx:89 +#: src/components/left-sidebar/top-area/settings-button.tsx:69 msgid "My Profile" msgstr "" @@ -843,6 +843,10 @@ msgstr "" #~ msgid "Pro" #~ msgstr "" +#: src/components/left-sidebar/top-area/settings-button.tsx:95 +msgid "Pro mode" +msgstr "" + #: src/components/settings/components/settings-header.tsx:22 msgid "Profile" msgstr "" @@ -918,7 +922,7 @@ msgstr "" msgid "Send invite" msgstr "" -#: src/components/left-sidebar/top-area/settings-button.tsx:82 +#: src/components/left-sidebar/top-area/settings-button.tsx:62 msgid "Settings" msgstr "" diff --git a/apps/docs/data/i18n.json b/apps/docs/data/i18n.json index 59888a954..ccd05ed8c 100644 --- a/apps/docs/data/i18n.json +++ b/apps/docs/data/i18n.json @@ -1,12 +1,12 @@ [ { "language": "ko", - "total": 259, - "missing": 259 + "total": 260, + "missing": 260 }, { "language": "en (source)", - "total": 259, + "total": 260, "missing": 0 } ] From 2ba5fe696bf82972f93d6bfaa143badae703a3e5 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 10 May 2025 19:49:01 -0700 Subject: [PATCH 20/26] fix build --- Cargo.lock | 12 +++++++- Cargo.toml | 1 + apps/app/server/Cargo.toml | 2 +- apps/app/server/src/native/subscription.rs | 2 +- plugins/membership-interface/Cargo.toml | 9 ++++++ plugins/membership-interface/src/lib.rs | 32 ++++++++++++++++++++++ plugins/membership/Cargo.toml | 2 ++ plugins/membership/src/commands.rs | 3 +- plugins/membership/src/ext.rs | 27 ++---------------- 9 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 plugins/membership-interface/Cargo.toml create mode 100644 plugins/membership-interface/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index dd96d7c38..83578e725 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "futures-core", "futures-util", "listener-interface", + "membership-interface", "nango", "notion", "openai", @@ -366,7 +367,6 @@ dependencies = [ "specta-typescript", "strum 0.26.3", "stt", - "tauri-plugin-membership", "thiserror 2.0.12", "tokio", "tower 0.5.2", @@ -7704,6 +7704,15 @@ dependencies = [ "regex", ] +[[package]] +name = "membership-interface" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", + "specta", +] + [[package]] name = "memchr" version = "2.7.4" @@ -13214,6 +13223,7 @@ dependencies = [ name = "tauri-plugin-membership" version = "0.1.0" dependencies = [ + "membership-interface", "reqwest 0.12.15", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index d6195c5f4..ccd662192 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ hypr-ws-utils = { path = "crates/ws-utils", package = "ws-utils" } hypr-auth-interface = { path = "plugins/auth-interface", package = "auth-interface" } hypr-listener-interface = { path = "plugins/listener-interface", package = "listener-interface" } +hypr-membership-interface = { path = "plugins/membership-interface", package = "membership-interface" } tauri = "2" tauri-build = "2" diff --git a/apps/app/server/Cargo.toml b/apps/app/server/Cargo.toml index 5ed2482d4..af8f0e2a8 100644 --- a/apps/app/server/Cargo.toml +++ b/apps/app/server/Cargo.toml @@ -9,7 +9,7 @@ dotenv = { workspace = true } [dependencies] hypr-auth-interface = { workspace = true } hypr-listener-interface = { workspace = true } -tauri-plugin-membership = { workspace = true } +hypr-membership-interface = { workspace = true } hypr-analytics = { workspace = true } hypr-buffer = { workspace = true } diff --git a/apps/app/server/src/native/subscription.rs b/apps/app/server/src/native/subscription.rs index 2419e8519..1d2408783 100644 --- a/apps/app/server/src/native/subscription.rs +++ b/apps/app/server/src/native/subscription.rs @@ -1,5 +1,5 @@ use axum::{http::StatusCode, Extension, Json}; -use tauri_plugin_membership::{Subscription, SubscriptionStatus}; +use hypr_membership_interface::{Subscription, SubscriptionStatus}; pub async fn handler( Extension(billing): Extension, diff --git a/plugins/membership-interface/Cargo.toml b/plugins/membership-interface/Cargo.toml new file mode 100644 index 000000000..12530e73a --- /dev/null +++ b/plugins/membership-interface/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "membership-interface" +version = "0.1.0" +edition = "2021" + +[dependencies] +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +specta = { workspace = true, features = ["derive"] } diff --git a/plugins/membership-interface/src/lib.rs b/plugins/membership-interface/src/lib.rs new file mode 100644 index 000000000..8a4cc51c3 --- /dev/null +++ b/plugins/membership-interface/src/lib.rs @@ -0,0 +1,32 @@ +#[macro_export] +macro_rules! common_derives { + ($item:item) => { + #[derive( + Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, specta::Type, + )] + $item + }; +} + +common_derives! { + pub struct Subscription { + pub status: SubscriptionStatus, + pub current_period_end: i64, + pub trial_end: Option, + pub price_id: Option, + } +} + +common_derives! { + #[serde(rename_all = "snake_case")] + pub enum SubscriptionStatus { + Active, + Canceled, + Incomplete, + IncompleteExpired, + PastDue, + Paused, + Trialing, + Unpaid, + } +} diff --git a/plugins/membership/Cargo.toml b/plugins/membership/Cargo.toml index 24bf7c353..6826fc595 100644 --- a/plugins/membership/Cargo.toml +++ b/plugins/membership/Cargo.toml @@ -15,6 +15,8 @@ specta-typescript = { workspace = true } tauri-plugin-store = { workspace = true } [dependencies] +hypr-membership-interface = { workspace = true } + tauri = { workspace = true, features = ["test"] } tauri-plugin-store2 = { workspace = true } tauri-specta = { workspace = true, features = ["derive", "typescript"] } diff --git a/plugins/membership/src/commands.rs b/plugins/membership/src/commands.rs index c570fb611..99bdc8396 100644 --- a/plugins/membership/src/commands.rs +++ b/plugins/membership/src/commands.rs @@ -1,4 +1,5 @@ -use crate::{MembershipPluginExt, Subscription}; +use crate::MembershipPluginExt; +use hypr_membership_interface::Subscription; #[tauri::command] #[specta::specta] diff --git a/plugins/membership/src/ext.rs b/plugins/membership/src/ext.rs index 34e55b08f..0e2f45fec 100644 --- a/plugins/membership/src/ext.rs +++ b/plugins/membership/src/ext.rs @@ -1,6 +1,8 @@ use std::future::Future; use tauri_plugin_store2::StorePluginExt; +use hypr_membership_interface::Subscription; + pub trait MembershipPluginExt { fn membership_store(&self) -> tauri_plugin_store2::ScopedStore; fn get_subscription(&self) -> impl Future, crate::Error>>; @@ -35,28 +37,3 @@ impl> MembershipPluginExt for T { Ok(data) } } - -#[derive( - Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, specta::Type, -)] -pub struct Subscription { - pub status: SubscriptionStatus, - pub current_period_end: i64, - pub trial_end: Option, - pub price_id: Option, -} - -#[derive( - Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, specta::Type, -)] -#[serde(rename_all = "snake_case")] -pub enum SubscriptionStatus { - Active, - Canceled, - Incomplete, - IncompleteExpired, - PastDue, - Paused, - Trialing, - Unpaid, -} From 07aad4e754723e14ff072b39ce89361a86c6185a Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 24 May 2025 14:10:55 -0700 Subject: [PATCH 21/26] chores --- Cargo.toml | 1 - apps/app/server/src/env.rs | 4 ++-- apps/desktop/src/routes/app.plans.tsx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 880ab5a0a..b5768c7f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,7 +114,6 @@ derive_more = "2" dirs = "6.0.0" dotenv = "0.15.0" dotenvy = "0.15.7" -dotenvy_macro = "0.15.7" envy = "0.4" include_url_macro = "0.1.0" indoc = "2" diff --git a/apps/app/server/src/env.rs b/apps/app/server/src/env.rs index 9aebce6f6..48ffc1797 100644 --- a/apps/app/server/src/env.rs +++ b/apps/app/server/src/env.rs @@ -1,8 +1,8 @@ pub fn load() -> ENV { #[cfg(debug_assertions)] - dotenv::from_filename(".env.local").unwrap(); + dotenv::from_filename(".env.local").ok(); - envy::from_env::().unwrap() + envy::from_env::().ok() } #[derive(Debug, serde::Deserialize)] diff --git a/apps/desktop/src/routes/app.plans.tsx b/apps/desktop/src/routes/app.plans.tsx index 741e258dc..4bac920b8 100644 --- a/apps/desktop/src/routes/app.plans.tsx +++ b/apps/desktop/src/routes/app.plans.tsx @@ -98,7 +98,7 @@ function PricingCard({
From e2f2496e9e0e8656b56cc74b75b6f05682fea6b2 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 24 May 2025 14:13:45 -0700 Subject: [PATCH 22/26] onboarding wip --- .../src/components/welcome-modal/index.tsx | 79 +------------------ .../welcome-modal/rating-display.tsx | 3 +- .../components/welcome-modal/welcome-view.tsx | 73 ++++++++++++++--- apps/desktop/src/routes/app.settings.tsx | 6 +- 4 files changed, 70 insertions(+), 91 deletions(-) diff --git a/apps/desktop/src/components/welcome-modal/index.tsx b/apps/desktop/src/components/welcome-modal/index.tsx index 733f53552..fd5e7159b 100644 --- a/apps/desktop/src/components/welcome-modal/index.tsx +++ b/apps/desktop/src/components/welcome-modal/index.tsx @@ -1,12 +1,7 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { useNavigate } from "@tanstack/react-router"; -import { message } from "@tauri-apps/plugin-dialog"; -import { openUrl } from "@tauri-apps/plugin-opener"; -import { useEffect, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { useEffect } from "react"; -import { baseUrl } from "@/client"; import { commands } from "@/types"; -import { commands as authCommands, events } from "@hypr/plugin-auth"; import { commands as localSttCommands, SupportedModel } from "@hypr/plugin-local-stt"; import { commands as sfxCommands } from "@hypr/plugin-sfx"; import { Modal, ModalBody } from "@hypr/ui/components/ui/modal"; @@ -21,48 +16,10 @@ interface WelcomeModalProps { } export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) { - const navigate = useNavigate(); - const [port, setPort] = useState(null); - const [showModelSelection, setShowModelSelection] = useState(false); - const selectSTTModel = useMutation({ mutationFn: (model: SupportedModel) => localSttCommands.setCurrentModel(model), }); - useEffect(() => { - let cleanup: (() => void) | undefined; - let unlisten: (() => void) | undefined; - - if (isOpen) { - authCommands.startOauthServer().then((port) => { - setPort(port); - - events.authEvent - .listen(({ payload }) => { - if (payload === "success") { - message("Successfully authenticated!"); - return; - } - - if (payload.error) { - message("Error occurred while authenticating!"); - return; - } - }) - .then((fn) => { - unlisten = fn; - }); - - cleanup = () => { - unlisten?.(); - authCommands.stopOauthServer(port); - }; - }); - } - - return () => cleanup?.(); - }, [isOpen, onClose, navigate]); - useEffect(() => { if (isOpen) { commands.setOnboardingNeeded(false); @@ -72,29 +29,6 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) { } }, [isOpen]); - const url = useQuery({ - queryKey: ["oauth-url", port], - enabled: !!port, - queryFn: () => { - const u = new URL(baseUrl); - u.pathname = "/auth/connect"; - u.searchParams.set("c", window.crypto.randomUUID()); - u.searchParams.set("f", "fingerprint"); - u.searchParams.set("p", port!.toString()); - return u.toString(); - }, - }); - - const _handleStartCloud = () => { - if (url.data) { - openUrl(url.data); - } - }; - - const handleStartLocal = () => { - setShowModelSelection(true); - }; - const handleModelSelected = (model: SupportedModel) => { selectSTTModel.mutate(model); onClose(); @@ -110,13 +44,8 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) { >
- {!showModelSelection - ? ( - - ) + {true + ? : ( void; -} +import { baseUrl } from "@/client"; +import { commands as authCommands, events } from "@hypr/plugin-auth"; +import PushableButton from "@hypr/ui/components/ui/pushable-button"; +import { TextAnimate } from "@hypr/ui/components/ui/text-animate"; -export const WelcomeView: React.FC = ({ portReady, onGetStarted }) => { +export const WelcomeView = () => { const { t } = useLingui(); + const [port, setPort] = useState(null); + + useEffect(() => { + let cleanup: (() => void) | undefined; + let unlisten: (() => void) | undefined; + + authCommands.startOauthServer().then((port) => { + setPort(port); + + events.authEvent + .listen(({ payload }) => { + if (payload === "success") { + message("Successfully authenticated!"); + return; + } + + if (payload.error) { + message("Error occurred while authenticating!"); + return; + } + }) + .then((fn) => { + unlisten = fn; + }); + + cleanup = () => { + unlisten?.(); + authCommands.stopOauthServer(port); + }; + }); + + return () => cleanup?.(); + }, []); + + const url = useQuery({ + queryKey: ["oauth-url", port], + enabled: !!port, + queryFn: () => { + const u = new URL(baseUrl); + u.pathname = "/auth/connect"; + u.searchParams.set("c", window.crypto.randomUUID()); + u.searchParams.set("f", "fingerprint"); + u.searchParams.set("p", port!.toString()); + return u.toString(); + }, + }); + + const handleStartCloud = () => { + if (url.data) { + openUrl(url.data); + } + }; return (
@@ -29,8 +82,8 @@ export const WelcomeView: React.FC = ({ portReady, onGetStarte Get Started diff --git a/apps/desktop/src/routes/app.settings.tsx b/apps/desktop/src/routes/app.settings.tsx index 603f8618c..384657b7b 100644 --- a/apps/desktop/src/routes/app.settings.tsx +++ b/apps/desktop/src/routes/app.settings.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { TabIcon } from "@/components/settings/components/tab-icon"; import { type Tab, TABS } from "@/components/settings/components/types"; -import { Calendar, Feedback, General, Lab, LocalAI, Notifications, Sound } from "@/components/settings/views"; +import { Calendar, Feedback, General, LocalAI, Notifications, Sound } from "@/components/settings/views"; import { cn } from "@hypr/ui/lib/utils"; const schema = z.object({ @@ -100,9 +100,7 @@ function Component() {
- {/* Main Content */}
- {/* Header */}
@@ -113,14 +111,12 @@ function Component() {
- {/* Actual Content */}
{search.tab === "general" && } {search.tab === "calendar" && } {search.tab === "notifications" && } {search.tab === "sound" && } {search.tab === "ai" && } - {search.tab === "lab" && } {search.tab === "feedback" && }
From 14d0bf469e551819ff43cd8cfa01cbd5ca278a13 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 24 May 2025 14:25:00 -0700 Subject: [PATCH 23/26] wip --- .../components/welcome-modal/welcome-view.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/components/welcome-modal/welcome-view.tsx b/apps/desktop/src/components/welcome-modal/welcome-view.tsx index 78f860ef7..c0d55b82b 100644 --- a/apps/desktop/src/components/welcome-modal/welcome-view.tsx +++ b/apps/desktop/src/components/welcome-modal/welcome-view.tsx @@ -64,6 +64,8 @@ export const WelcomeView = () => { } }; + const handleStartLocal = () => {}; + return (
{ {t`The AI Meeting Notepad`} - - Get Started - +
+ + Get Started + + + +
); }; From ffbdf83250e43b03c6204853a3634d6746e0e9df Mon Sep 17 00:00:00 2001 From: yujonglee Date: Sat, 24 May 2025 14:46:49 -0700 Subject: [PATCH 24/26] wip --- .../src/components/welcome-modal/index.tsx | 8 ++- .../welcome-modal/model-selection-view.tsx | 58 +++++++++---------- .../components/welcome-modal/welcome-view.tsx | 6 +- apps/desktop/src/routes/app.tsx | 2 +- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/apps/desktop/src/components/welcome-modal/index.tsx b/apps/desktop/src/components/welcome-modal/index.tsx index fd5e7159b..f6ec99771 100644 --- a/apps/desktop/src/components/welcome-modal/index.tsx +++ b/apps/desktop/src/components/welcome-modal/index.tsx @@ -1,5 +1,5 @@ import { useMutation } from "@tanstack/react-query"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { commands } from "@/types"; import { commands as localSttCommands, SupportedModel } from "@hypr/plugin-local-stt"; @@ -16,6 +16,8 @@ interface WelcomeModalProps { } export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) { + const [step, setStep] = useState<"0_login" | "1_model-selection">("0_login"); + const selectSTTModel = useMutation({ mutationFn: (model: SupportedModel) => localSttCommands.setCurrentModel(model), }); @@ -44,8 +46,8 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) { >
- {true - ? + {step === "0_login" + ? setStep("1_model-selection")} /> : ( ( -
- {label} -
- {[...Array(maxRating)].map((_, i) => ( - - ))} -
-
-); - export const ModelSelectionView = ({ onContinue, }: { @@ -55,7 +26,10 @@ export const ModelSelectionView = ({ }) => { const [selectedModel, setSelectedModel] = useState("QuantizedSmall"); - const supportedSTTModels = useQuery({ + const supportedSTTModels = useQuery<{ + model: string; + is_downloaded: boolean; + }[]>({ queryKey: ["local-stt", "supported-models"], queryFn: async () => { const models = await localSttCommands.listSupportedModels(); @@ -153,3 +127,27 @@ export const ModelSelectionView = ({
); }; + +const RatingDisplay = ( + { label, rating, maxRating = 3, icon: Icon }: { + label: string; + rating: number; + maxRating?: number; + icon: React.ElementType; + }, +) => ( +
+ {label} +
+ {[...Array(maxRating)].map((_, i) => ( + + ))} +
+
+); diff --git a/apps/desktop/src/components/welcome-modal/welcome-view.tsx b/apps/desktop/src/components/welcome-modal/welcome-view.tsx index c0d55b82b..00f5f1041 100644 --- a/apps/desktop/src/components/welcome-modal/welcome-view.tsx +++ b/apps/desktop/src/components/welcome-modal/welcome-view.tsx @@ -9,7 +9,7 @@ import { commands as authCommands, events } from "@hypr/plugin-auth"; import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { TextAnimate } from "@hypr/ui/components/ui/text-animate"; -export const WelcomeView = () => { +export const WelcomeView = ({ onContinue }: { onContinue: () => void }) => { const { t } = useLingui(); const [port, setPort] = useState(null); @@ -64,7 +64,9 @@ export const WelcomeView = () => { } }; - const handleStartLocal = () => {}; + const handleStartLocal = () => { + onContinue(); + }; return (
diff --git a/apps/desktop/src/routes/app.tsx b/apps/desktop/src/routes/app.tsx index e43aa8031..7602de4b4 100644 --- a/apps/desktop/src/routes/app.tsx +++ b/apps/desktop/src/routes/app.tsx @@ -71,7 +71,7 @@ function Component() {
{ commands.setOnboardingNeeded(false); router.invalidate(); From aa7bc7b45789cb6b42e2c3db1d9442ddf1ad3821 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 30 May 2025 17:59:56 -0700 Subject: [PATCH 25/26] update lock --- pnpm-lock.yaml | 98 +++++++++++++++++++------------------------------- 1 file changed, 36 insertions(+), 62 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23e3d75ee..ad8526a3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,7 +405,7 @@ importers: version: 2.2.5(mime-types@3.0.1)(vite@5.4.19(@types/node@22.15.21)) vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3)) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(tsx@4.19.4)(yaml@2.8.0) apps/docs: dependencies: @@ -504,16 +504,16 @@ importers: version: link:../utils '@remixicon/react': specifier: ^4.6.0 - version: 4.6.0(react@19.0.0) + version: 4.6.0(react@18.3.1) '@sereneinserenade/tiptap-search-and-replace': specifier: ^0.1.1 version: 0.1.1(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0) '@tanstack/react-query': specifier: ^5.76.2 - version: 5.76.2(react@19.0.0) + version: 5.76.2(react@18.3.1) '@tanstack/react-router': specifier: ^1.120.5 - version: 1.120.5(react-dom@18.3.1(react@19.0.0))(react@19.0.0) + version: 1.120.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/core': specifier: ^2.12.0 version: 2.12.0(@tiptap/pm@2.12.0) @@ -567,7 +567,7 @@ importers: version: 2.12.0 '@tiptap/react': specifier: ^2.12.0 - version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@18.3.1(react@19.0.0))(react@19.0.0) + version: 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tiptap/starter-kit': specifier: ^2.12.0 version: 2.12.0 @@ -579,7 +579,7 @@ importers: version: 2.1.1 lucide-react: specifier: ^0.511.0 - version: 0.511.0(react@19.0.0) + version: 0.511.0(react@18.3.1) prosemirror-commands: specifier: ^1.7.1 version: 1.7.1 @@ -591,10 +591,10 @@ importers: version: 1.4.3 react: specifier: ^18.3.1 - version: 19.0.0 + version: 18.3.1 react-dom: specifier: ^18.3.1 - version: 18.3.1(react@19.0.0) + version: 18.3.1(react@18.3.1) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -616,7 +616,7 @@ importers: version: 5.0.5 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3)) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(tsx@4.19.4)(yaml@2.8.0) packages/ui: dependencies: @@ -807,7 +807,7 @@ importers: version: 18.3.22 vitest: specifier: ^3.1.4 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3)) + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(tsx@4.19.4)(yaml@2.8.0) plugins/analytics: dependencies: @@ -9464,10 +9464,6 @@ snapshots: dependencies: react: 18.3.1 - '@remixicon/react@4.6.0(react@19.0.0)': - dependencies: - react: 19.0.0 - '@rollup/pluginutils@5.1.4(rollup@4.41.0)': dependencies: '@types/estree': 1.0.7 @@ -9745,17 +9741,6 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-router@1.120.5(react-dom@18.3.1(react@19.0.0))(react@19.0.0)': - dependencies: - '@tanstack/history': 1.115.0 - '@tanstack/react-store': 0.7.0(react-dom@18.3.1(react@19.0.0))(react@19.0.0) - '@tanstack/router-core': 1.120.5 - jsesc: 3.1.0 - react: 19.0.0 - react-dom: 18.3.1(react@19.0.0) - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/store': 0.7.0 @@ -9763,13 +9748,6 @@ snapshots: react-dom: 18.3.1(react@18.3.1) use-sync-external-store: 1.5.0(react@18.3.1) - '@tanstack/react-store@0.7.0(react-dom@18.3.1(react@19.0.0))(react@19.0.0)': - dependencies: - '@tanstack/store': 0.7.0 - react: 19.0.0 - react-dom: 18.3.1(react@19.0.0) - use-sync-external-store: 1.5.0(react@19.0.0) - '@tanstack/router-core@1.120.5': dependencies: '@tanstack/history': 1.115.0 @@ -10123,7 +10101,7 @@ snapshots: prosemirror-transform: 1.10.4 prosemirror-view: 1.39.3 - '@tiptap/react@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@18.3.1(react@19.0.0))(react@19.0.0)': + '@tiptap/react@2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tiptap/core': 2.12.0(@tiptap/pm@2.12.0) '@tiptap/extension-bubble-menu': 2.12.0(@tiptap/core@2.12.0(@tiptap/pm@2.12.0))(@tiptap/pm@2.12.0) @@ -10131,9 +10109,9 @@ snapshots: '@tiptap/pm': 2.12.0 '@types/use-sync-external-store': 0.0.6 fast-deep-equal: 3.1.3 - react: 19.0.0 - react-dom: 18.3.1(react@19.0.0) - use-sync-external-store: 1.5.0(react@19.0.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-sync-external-store: 1.5.0(react@18.3.1) '@tiptap/starter-kit@2.12.0': dependencies: @@ -10577,14 +10555,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.1.4(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(vite@5.4.19(@types/node@22.15.21))': + '@vitest/mocker@3.1.4(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.1.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.8.4(@types/node@22.15.21)(typescript@5.8.3) - vite: 5.4.19(@types/node@22.15.21) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) '@vitest/pretty-format@2.1.9': dependencies: @@ -12040,7 +12018,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.3.4 + debug: 4.4.1(supports-color@8.1.1) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -12291,7 +12269,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.3.4 + debug: 4.4.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -12338,7 +12316,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.8 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 @@ -13016,9 +12994,9 @@ snapshots: dependencies: react: 18.3.1 - lucide-react@0.511.0(react@19.0.0): + lucide-react@0.511.0(react@18.3.1): dependencies: - react: 19.0.0 + react: 18.3.1 lunr@2.3.9: {} @@ -13488,7 +13466,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.3 - debug: 4.3.4 + debug: 4.4.1(supports-color@8.1.1) get-uri: 6.0.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -13816,7 +13794,7 @@ snapshots: proxy-agent@6.3.1: dependencies: agent-base: 7.1.3 - debug: 4.3.4 + debug: 4.4.1(supports-color@8.1.1) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 lru-cache: 7.18.3 @@ -13884,12 +13862,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-dom@18.3.1(react@19.0.0): - dependencies: - loose-envify: 1.4.0 - react: 19.0.0 - scheduler: 0.23.2 - react-draggable@4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 @@ -14369,7 +14341,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.3 - debug: 4.3.4 + debug: 4.4.1(supports-color@8.1.1) socks: 2.8.4 transitivePeerDependencies: - supports-color @@ -15062,10 +15034,6 @@ snapshots: dependencies: react: 18.3.1 - use-sync-external-store@1.5.0(react@19.0.0): - dependencies: - react: 19.0.0 - userhome@1.0.1: {} util-deprecate@1.0.2: {} @@ -15097,15 +15065,16 @@ snapshots: vite: 5.4.19(@types/node@22.15.21) watchpack: 2.4.4 - vite-node@3.1.4(@types/node@22.15.21): + vite-node@3.1.4(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 5.4.19(@types/node@22.15.21) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' + - jiti - less - lightningcss - sass @@ -15114,6 +15083,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml vite-plugin-posthog@2.1.0(@types/react@18.3.22)(react@18.3.1): dependencies: @@ -15196,10 +15167,10 @@ snapshots: - universal-cookie - yaml - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3)): + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.21)(jiti@2.4.2)(jsdom@25.0.1)(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(tsx@4.19.4)(yaml@2.8.0): dependencies: '@vitest/expect': 3.1.4 - '@vitest/mocker': 3.1.4(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(vite@5.4.19(@types/node@22.15.21)) + '@vitest/mocker': 3.1.4(msw@2.8.4(@types/node@22.15.21)(typescript@5.8.3))(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0)) '@vitest/pretty-format': 3.1.4 '@vitest/runner': 3.1.4 '@vitest/snapshot': 3.1.4 @@ -15216,14 +15187,15 @@ snapshots: tinyglobby: 0.2.13 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.19(@types/node@22.15.21) - vite-node: 3.1.4(@types/node@22.15.21) + vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) + vite-node: 3.1.4(@types/node@22.15.21)(jiti@2.4.2)(tsx@4.19.4)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 '@types/node': 22.15.21 jsdom: 25.0.1 transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -15233,6 +15205,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml vue-flow-layout@0.1.1(vue@3.5.14(typescript@5.8.3)): dependencies: From 6a89a6c7bdcda8fe2f2071eefe24abd8ce96afd3 Mon Sep 17 00:00:00 2001 From: yujonglee Date: Fri, 30 May 2025 18:17:18 -0700 Subject: [PATCH 26/26] fix type --- apps/app/server/src/env.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/server/src/env.rs b/apps/app/server/src/env.rs index 48ffc1797..b4c3a2ce0 100644 --- a/apps/app/server/src/env.rs +++ b/apps/app/server/src/env.rs @@ -2,7 +2,7 @@ pub fn load() -> ENV { #[cfg(debug_assertions)] dotenv::from_filename(".env.local").ok(); - envy::from_env::().ok() + envy::from_env::().unwrap() } #[derive(Debug, serde::Deserialize)]