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() {

{panel.title}

- {isLoading - ? panel.suffixes.map((suffix) => ( - - {ASSET_LABELS[suffix]} - - )) - : panelAssets.map((asset) => ( - - {getAssetLabel(asset.name)} - - ))} + {panelAssets.map((asset) => ( + + {getAssetLabel(asset.name)} + + ))}
); 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 ( -
+
@@ -261,12 +263,12 @@ export function TrialForm() {

- {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 - -