diff --git a/surfsense_local/scripts/bump-version.sh b/surfsense_local/scripts/bump-version.sh
index 7c102a5a72..9b65212366 100755
--- a/surfsense_local/scripts/bump-version.sh
+++ b/surfsense_local/scripts/bump-version.sh
@@ -18,6 +18,7 @@ fi
JSON_FIELD='"version"[[:space:]]*:[[:space:]]*"[^"]*"'
TOML_FIELD='^version[[:space:]]*=[[:space:]]*"[^"]*"'
+TS_FIELD='^export const APP_RELEASE_VERSION = "[^"]*"'
echo "Bumping surfsense_local to $VERSION"
echo "---------------------------------"
@@ -52,6 +53,8 @@ bump() {
bump "$LOCAL_ROOT/backend/pyproject.toml" "$TOML_FIELD" "version = \"$VERSION\""
bump "$LOCAL_ROOT/frontend/package.json" "$JSON_FIELD" "\"version\": \"$VERSION\""
bump "$LOCAL_ROOT/electron/package.json" "$JSON_FIELD" "\"version\": \"$VERSION\""
+bump "$LOCAL_ROOT/../surfsense_web/lib/app-release.ts" "$TS_FIELD" \
+ "export const APP_RELEASE_VERSION = \"$VERSION\""
echo ""
echo "Syncing lock files..."
diff --git a/surfsense_web/app/(home)/downloads/download-panels.tsx b/surfsense_web/app/(home)/downloads/download-panels.tsx
index 27117f8347..042329b6b2 100644
--- a/surfsense_web/app/(home)/downloads/download-panels.tsx
+++ b/surfsense_web/app/(home)/downloads/download-panels.tsx
@@ -1,61 +1,19 @@
-"use client";
-
-import { FlowButton } from "@/components/ui/flow-button";
import { DownloadIcon } from "@/components/ui/icons";
-import {
- ASSET_LABELS,
- GITHUB_RELEASES_URL,
- getAssetLabel,
- useLatestRelease,
- usePrimaryDownload,
-} from "@/lib/desktop-download-utils";
+import { GITHUB_RELEASES_URL, getAssetLabel, type ReleaseAsset } from "@/lib/app-release";
/**
- * The two pieces of `lib/desktop-download-utils.ts` this page needs, split
- * into client components so `page.tsx` can stay a server component: the
- * hero's single auto-detected button, and the three-way OS grid below it.
+ * Server components: the assets are resolved in `page.tsx` and handed down,
+ * so the installer links are in the HTML rather than appearing after
+ * hydration. This page is an SEO target, and a crawler used to see an empty
+ * grid.
*/
-export function PrimaryDownloadButton() {
- const { os, primary, isMobileOS, isLoading } = usePrimaryDownload();
-
- if (isMobileOS) {
- return (
-
- The desktop app is not available on {os}. Browse{" "}
-
- all releases
- {" "}
- instead.
-
- );
- }
-
- if (isLoading) {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
type OSPanel = {
title: string;
match: (assetName: string) => boolean;
- /** Asset-label suffixes expected for this platform, used to size the
- * disabled placeholder links while the real list is still loading. */
- suffixes: (keyof typeof ASSET_LABELS)[];
+ /** Sort order within a panel: GitHub does not promise a stable asset
+ * order across releases. */
+ suffixes: string[];
};
const OS_PANELS: OSPanel[] = [
@@ -84,16 +42,10 @@ export function AllReleasesLink() {
);
}
-export function OSDownloadGrid() {
- const { assets, isLoading } = useLatestRelease();
-
+export function OSDownloadGrid({ assets }: { assets: ReleaseAsset[] }) {
return (
{OS_PANELS.map((panel) => {
- // Sorted to the same fixed order as the loading placeholders below,
- // since GitHub doesn't guarantee asset order is stable across
- // releases — without this the real links can swap position right
- // as they replace the placeholders.
const panelAssets = assets
.filter((asset) => panel.match(asset.name))
.toSorted(
@@ -105,23 +57,12 @@ export function OSDownloadGrid() {
);
diff --git a/surfsense_web/app/(home)/downloads/page.tsx b/surfsense_web/app/(home)/downloads/page.tsx
index 9e6fb8f212..0780a28ae6 100644
--- a/surfsense_web/app/(home)/downloads/page.tsx
+++ b/surfsense_web/app/(home)/downloads/page.tsx
@@ -1,17 +1,13 @@
import type { Metadata } from "next";
-import { AllReleasesLink, OSDownloadGrid, PrimaryDownloadButton } from "./download-panels";
+import { TrialForm } from "@/app/(home)/license/license-forms";
+import { getReleaseAssets } from "@/lib/release-assets";
+import { AllReleasesLink, OSDownloadGrid } from "./download-panels";
/**
* Rendered in the site design: the palette, ruled column, navigation and
* footer all come from `app/(home)/layout.tsx`, and every style resolves from
* `app/(home)/home.css`. Listed in `SITE_DESIGN_ROUTES` in
* `components/site/site-shell.tsx`.
- *
- * The hero's single button auto-detects the visitor's OS and links straight
- * to the matching installer (`PrimaryDownloadButton`, from
- * `lib/desktop-download-utils.ts` — the same hook the old homepage hero
- * used); the grid below it is the explicit fallback for anyone downloading
- * for a machine other than the one they're on.
*/
export const metadata: Metadata = {
@@ -21,7 +17,9 @@ export const metadata: Metadata = {
alternates: { canonical: "https://www.surfsense.com/downloads" },
};
-export default function DownloadsPage() {
+export default async function DownloadsPage() {
+ const assets = await getReleaseAssets();
+
return (
<>
@@ -30,20 +28,20 @@ export default function DownloadsPage() {
One installer, no account, no cloud. Pick your platform below.
-
-
-
+
-
-
-
Choose your platform
-
Windows, macOS and Linux
-
+ {assets.length > 0 ? (
+
+
+
Choose your platform
+
Windows, macOS and Linux
+
-
-
+
+
+ ) : null}
diff --git a/surfsense_web/app/(home)/license/license-forms.tsx b/surfsense_web/app/(home)/license/license-forms.tsx
index 9519cbc556..88d7ed1cbc 100644
--- a/surfsense_web/app/(home)/license/license-forms.tsx
+++ b/surfsense_web/app/(home)/license/license-forms.tsx
@@ -204,7 +204,9 @@ export function ResendDisclosure() {
);
}
-export function TrialForm() {
+const TRIAL_NOTE = "One trial per email address. We will send the license file to your inbox.";
+
+export function TrialForm({ note = TRIAL_NOTE }: { note?: string } = {}) {
const id = useId();
const [email, setEmail] = useState("");
const [busy, setBusy] = useState(false);
@@ -251,7 +253,7 @@ export function TrialForm() {
}
return (
-
+
- {outcome
- ? outcome.message
- : "One trial per email address. We will send the license file to your inbox."}
+ {outcome ? outcome.message : note}
);
diff --git a/surfsense_web/app/(home)/sunset/layout.tsx b/surfsense_web/app/(home)/sunset/layout.tsx
new file mode 100644
index 0000000000..91e65f910f
--- /dev/null
+++ b/surfsense_web/app/(home)/sunset/layout.tsx
@@ -0,0 +1,5 @@
+import { RuntimeConfig } from "@/components/providers/runtime-config.server";
+
+export default function SunsetLayout({ children }: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/surfsense_web/app/(home)/sunset/sunset-export.tsx b/surfsense_web/app/(home)/sunset/sunset-export.tsx
index 7081c5738a..b3b2d598cb 100644
--- a/surfsense_web/app/(home)/sunset/sunset-export.tsx
+++ b/surfsense_web/app/(home)/sunset/sunset-export.tsx
@@ -3,11 +3,13 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { HomeButton } from "@/components/homepage/home/home-button";
+import { useIsGoogleAuth } from "@/components/providers/runtime-config";
import { Spinner } from "@/components/ui/spinner";
import { useSession } from "@/hooks/use-session";
import { authenticatedFetch } from "@/lib/auth-fetch";
import { redirectToLogin } from "@/lib/auth-utils";
import { buildBackendUrl } from "@/lib/env-config";
+import { trackLoginAttempt } from "@/lib/posthog/events";
/**
* Rendered inline in the hero on `/sunset` rather than as its own section —
@@ -41,8 +43,21 @@ function triggerDownload(blob: Blob, filename: string) {
export function SunsetExport() {
const session = useSession();
+ const isGoogleAuth = useIsGoogleAuth();
const [isExporting, setIsExporting] = useState(false);
+ // Google-only deployments have nothing to choose on /login: it renders a
+ // lone Google button. Export is the one thing this page exists for, so
+ // send them straight to the provider instead of through that page.
+ function signIn() {
+ if (!isGoogleAuth) {
+ redirectToLogin();
+ return;
+ }
+ trackLoginAttempt("google");
+ window.location.href = buildBackendUrl("/auth/google/authorize-redirect");
+ }
+
// Session status resolves asynchronously and can settle before hydration
// finishes, so deriving `disabled`/label straight from it made the first
// client render disagree with the server-rendered HTML. Gating on mount
@@ -54,7 +69,7 @@ export function SunsetExport() {
async function handleExport() {
if (isExporting) return;
if (session.status !== "authenticated") {
- redirectToLogin();
+ signIn();
return;
}
@@ -65,7 +80,7 @@ export function SunsetExport() {
skipAuthRedirect: true,
});
if (response.status === 401) {
- redirectToLogin();
+ signIn();
return;
}
if (!response.ok) {
diff --git a/surfsense_web/components/pricing/pricing-content.ts b/surfsense_web/components/pricing/pricing-content.ts
index 08abae43ad..32ff5048c6 100644
--- a/surfsense_web/components/pricing/pricing-content.ts
+++ b/surfsense_web/components/pricing/pricing-content.ts
@@ -86,7 +86,7 @@ export const PLANS: Plan[] = [
],
action: [
{ label: "Buy a licence", href: BUY_INDIVIDUAL_URL, external: true, primary: true },
- { label: "Start a trial", href: LICENSE_URL },
+ { label: "Start a 30-day trial", href: LICENSE_URL },
],
featured: true,
},
diff --git a/surfsense_web/components/site/site-content.ts b/surfsense_web/components/site/site-content.ts
index 331427e745..3cb80d47db 100644
--- a/surfsense_web/components/site/site-content.ts
+++ b/surfsense_web/components/site/site-content.ts
@@ -9,7 +9,6 @@
export const REPO_URL = "https://github.com/MODSetter/SurfSense";
export const DOWNLOADS_URL = "/downloads";
-export const SIGN_IN_URL = "/login";
export type SiteLink = { name: string; href: string; external?: boolean };
export type SiteMenuItem = SiteLink & { description: string };
@@ -53,7 +52,6 @@ export const FOOTER_COLUMNS: { heading: string; links: FooterLink[] }[] = [
links: [
{ title: "Privacy Policy", href: "/privacy" },
{ title: "Terms of Service", href: "/terms" },
- { title: "Sign in", href: SIGN_IN_URL },
],
},
];
diff --git a/surfsense_web/components/site/site-nav.tsx b/surfsense_web/components/site/site-nav.tsx
index 5930f2513f..364e5da97e 100644
--- a/surfsense_web/components/site/site-nav.tsx
+++ b/surfsense_web/components/site/site-nav.tsx
@@ -4,8 +4,7 @@ import { IconChevronDown, IconMenu2, IconX } from "@tabler/icons-react";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
-import { HomeButton } from "@/components/homepage/home/home-button";
-import { NAV_LINKS, NAV_RESOURCES, SIGN_IN_URL } from "@/components/site/site-content";
+import { NAV_LINKS, NAV_RESOURCES } from "@/components/site/site-content";
import { SiteStars } from "@/components/site/site-stars";
import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
@@ -149,13 +148,6 @@ export function SiteNav({ starCount, starsHref }: { starCount: number | null; st
- {/* The bar's one action is signing in. Downloading is the landing
- page's job: repeating it here would put two primary calls to
- action on the same screen, competing with each other. */}
-
- Sign in
-
-