Merge remote-tracking branch 'upstream/dev' into fix/docker-host-gateway

This commit is contained in:
Anish Sarkar 2026-04-21 11:24:34 +05:30
commit f06e00d77c
28 changed files with 1252 additions and 155 deletions

View file

@ -1,6 +1,6 @@
"use client";
import { BrainCog, Rocket, Zap } from "lucide-react";
import { BrainCog, Power, Rocket, Zap } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { DEFAULT_SHORTCUTS, ShortcutRecorder } from "@/components/desktop/shortcut-recorder";
@ -30,6 +30,10 @@ export function DesktopContent() {
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
const [activeSpaceId, setActiveSpaceId] = useState<string | null>(null);
const [autoLaunchEnabled, setAutoLaunchEnabled] = useState(false);
const [autoLaunchHidden, setAutoLaunchHidden] = useState(true);
const [autoLaunchSupported, setAutoLaunchSupported] = useState(false);
useEffect(() => {
if (!api) {
setLoading(false);
@ -38,19 +42,28 @@ export function DesktopContent() {
}
let mounted = true;
const hasAutoLaunchApi =
typeof api.getAutoLaunch === "function" && typeof api.setAutoLaunch === "function";
setAutoLaunchSupported(hasAutoLaunchApi);
Promise.all([
api.getAutocompleteEnabled(),
api.getShortcuts?.() ?? Promise.resolve(null),
api.getActiveSearchSpace?.() ?? Promise.resolve(null),
searchSpacesApiService.getSearchSpaces(),
hasAutoLaunchApi ? api.getAutoLaunch() : Promise.resolve(null),
])
.then(([autoEnabled, config, spaceId, spaces]) => {
.then(([autoEnabled, config, spaceId, spaces, autoLaunch]) => {
if (!mounted) return;
setEnabled(autoEnabled);
if (config) setShortcuts(config);
setActiveSpaceId(spaceId);
if (spaces) setSearchSpaces(spaces);
if (autoLaunch) {
setAutoLaunchEnabled(autoLaunch.enabled);
setAutoLaunchHidden(autoLaunch.openAsHidden);
setAutoLaunchSupported(autoLaunch.supported);
}
setLoading(false);
setShortcutsLoaded(true);
})
@ -106,6 +119,40 @@ export function DesktopContent() {
updateShortcut(key, DEFAULT_SHORTCUTS[key]);
};
const handleAutoLaunchToggle = async (checked: boolean) => {
if (!autoLaunchSupported || !api.setAutoLaunch) {
toast.error("Please update the desktop app to configure launch on startup");
return;
}
setAutoLaunchEnabled(checked);
try {
const next = await api.setAutoLaunch(checked, autoLaunchHidden);
if (next) {
setAutoLaunchEnabled(next.enabled);
setAutoLaunchHidden(next.openAsHidden);
setAutoLaunchSupported(next.supported);
}
toast.success(checked ? "SurfSense will launch on startup" : "Launch on startup disabled");
} catch {
setAutoLaunchEnabled(!checked);
toast.error("Failed to update launch on startup");
}
};
const handleAutoLaunchHiddenToggle = async (checked: boolean) => {
if (!autoLaunchSupported || !api.setAutoLaunch) {
toast.error("Please update the desktop app to configure startup behavior");
return;
}
setAutoLaunchHidden(checked);
try {
await api.setAutoLaunch(autoLaunchEnabled, checked);
} catch {
setAutoLaunchHidden(!checked);
toast.error("Failed to update startup behavior");
}
};
const handleSearchSpaceChange = (value: string) => {
setActiveSpaceId(value);
api.setActiveSearchSpace?.(value);
@ -145,6 +192,60 @@ export function DesktopContent() {
</CardContent>
</Card>
{/* Launch on Startup */}
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">
<CardTitle className="text-base md:text-lg flex items-center gap-2">
<Power className="h-4 w-4" />
Launch on Startup
</CardTitle>
<CardDescription className="text-xs md:text-sm">
Automatically start SurfSense when you sign in to your computer so global
shortcuts and folder sync are always available.
</CardDescription>
</CardHeader>
<CardContent className="px-3 md:px-6 pb-3 md:pb-6 space-y-3">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label htmlFor="auto-launch-toggle" className="text-sm font-medium cursor-pointer">
Open SurfSense at login
</Label>
<p className="text-xs text-muted-foreground">
{autoLaunchSupported
? "Adds SurfSense to your system's login items."
: "Only available in the packaged desktop app."}
</p>
</div>
<Switch
id="auto-launch-toggle"
checked={autoLaunchEnabled}
onCheckedChange={handleAutoLaunchToggle}
disabled={!autoLaunchSupported}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label
htmlFor="auto-launch-hidden-toggle"
className="text-sm font-medium cursor-pointer"
>
Start minimized to tray
</Label>
<p className="text-xs text-muted-foreground">
Skip the main window on boot SurfSense lives in the system tray until you need
it.
</p>
</div>
<Switch
id="auto-launch-hidden-toggle"
checked={autoLaunchHidden}
onCheckedChange={handleAutoLaunchHiddenToggle}
disabled={!autoLaunchSupported || !autoLaunchEnabled}
/>
</div>
</CardContent>
</Card>
{/* Keyboard Shortcuts */}
<Card>
<CardHeader className="px-3 md:px-6 pt-3 md:pt-6 pb-2 md:pb-3">

View file

@ -1,7 +1,8 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { ReceiptText } from "lucide-react";
import { useQueries } from "@tanstack/react-query";
import { Coins, FileText, ReceiptText } from "lucide-react";
import { useMemo } from "react";
import { Badge } from "@/components/ui/badge";
import { Spinner } from "@/components/ui/spinner";
import {
@ -12,10 +13,26 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { PagePurchase, PagePurchaseStatus } from "@/contracts/types/stripe.types";
import type {
PagePurchase,
PagePurchaseStatus,
TokenPurchase,
} from "@/contracts/types/stripe.types";
import { stripeApiService } from "@/lib/apis/stripe-api.service";
import { cn } from "@/lib/utils";
type PurchaseKind = "pages" | "tokens";
type UnifiedPurchase = {
id: string;
kind: PurchaseKind;
created_at: string;
status: PagePurchaseStatus;
granted: number;
amount_total: number | null;
currency: string | null;
};
const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: string }> = {
completed: {
label: "Completed",
@ -31,6 +48,22 @@ const STATUS_STYLES: Record<PagePurchaseStatus, { label: string; className: stri
},
};
const KIND_META: Record<
PurchaseKind,
{ label: string; icon: React.ComponentType<{ className?: string }>; iconClass: string }
> = {
pages: {
label: "Pages",
icon: FileText,
iconClass: "text-sky-500",
},
tokens: {
label: "Premium Tokens",
icon: Coins,
iconClass: "text-amber-500",
},
};
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString(undefined, {
year: "numeric",
@ -39,19 +72,65 @@ function formatDate(iso: string): string {
});
}
function formatAmount(purchase: PagePurchase): string {
if (purchase.amount_total == null) return "—";
const dollars = purchase.amount_total / 100;
const currency = (purchase.currency ?? "usd").toUpperCase();
return `$${dollars.toFixed(2)} ${currency}`;
function formatAmount(amount: number | null, currency: string | null): string {
if (amount == null) return "—";
const dollars = amount / 100;
const code = (currency ?? "usd").toUpperCase();
return `$${dollars.toFixed(2)} ${code}`;
}
function normalizePagePurchase(p: PagePurchase): UnifiedPurchase {
return {
id: p.id,
kind: "pages",
created_at: p.created_at,
status: p.status,
granted: p.pages_granted,
amount_total: p.amount_total,
currency: p.currency,
};
}
function normalizeTokenPurchase(p: TokenPurchase): UnifiedPurchase {
return {
id: p.id,
kind: "tokens",
created_at: p.created_at,
status: p.status,
granted: p.tokens_granted,
amount_total: p.amount_total,
currency: p.currency,
};
}
export function PurchaseHistoryContent() {
const { data, isLoading } = useQuery({
queryKey: ["stripe-purchases"],
queryFn: () => stripeApiService.getPurchases(),
const results = useQueries({
queries: [
{
queryKey: ["stripe-purchases"],
queryFn: () => stripeApiService.getPurchases(),
},
{
queryKey: ["stripe-token-purchases"],
queryFn: () => stripeApiService.getTokenPurchases(),
},
],
});
const [pagesQuery, tokensQuery] = results;
const isLoading = pagesQuery.isLoading || tokensQuery.isLoading;
const purchases = useMemo<UnifiedPurchase[]>(() => {
const pagePurchases = pagesQuery.data?.purchases ?? [];
const tokenPurchases = tokensQuery.data?.purchases ?? [];
return [
...pagePurchases.map(normalizePagePurchase),
...tokenPurchases.map(normalizeTokenPurchase),
].sort(
(a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
}, [pagesQuery.data, tokensQuery.data]);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
@ -60,15 +139,13 @@ export function PurchaseHistoryContent() {
);
}
const purchases = data?.purchases ?? [];
if (purchases.length === 0) {
return (
<div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
<ReceiptText className="h-8 w-8 text-muted-foreground" />
<p className="text-sm font-medium">No purchases yet</p>
<p className="text-xs text-muted-foreground">
Your page-pack purchases will appear here after checkout.
Your page and premium token purchases will appear here after checkout.
</p>
</div>
);
@ -81,25 +158,36 @@ export function PurchaseHistoryContent() {
<TableHeader>
<TableRow>
<TableHead>Date</TableHead>
<TableHead className="text-right">Pages</TableHead>
<TableHead>Type</TableHead>
<TableHead className="text-right">Granted</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{purchases.map((p) => {
const style = STATUS_STYLES[p.status];
const statusStyle = STATUS_STYLES[p.status];
const kind = KIND_META[p.kind];
const KindIcon = kind.icon;
return (
<TableRow key={p.id}>
<TableRow key={`${p.kind}-${p.id}`}>
<TableCell className="text-sm">{formatDate(p.created_at)}</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{p.pages_granted.toLocaleString()}
<TableCell className="text-sm">
<div className="flex items-center gap-2">
<KindIcon className={cn("h-4 w-4", kind.iconClass)} />
<span>{kind.label}</span>
</div>
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{formatAmount(p)}
{p.granted.toLocaleString()}
</TableCell>
<TableCell className="text-right tabular-nums text-sm">
{formatAmount(p.amount_total, p.currency)}
</TableCell>
<TableCell className="text-center">
<Badge className={cn("text-[10px]", style.className)}>{style.label}</Badge>
<Badge className={cn("text-[10px]", statusStyle.className)}>
{statusStyle.label}
</Badge>
</TableCell>
</TableRow>
);
@ -108,7 +196,8 @@ export function PurchaseHistoryContent() {
</Table>
</div>
<p className="text-center text-xs text-muted-foreground">
Showing your {purchases.length} most recent purchase{purchases.length !== 1 ? "s" : ""}.
Showing your {purchases.length} most recent purchase
{purchases.length !== 1 ? "s" : ""}.
</p>
</div>
);