Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions surfsense_local/scripts/bump-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "---------------------------------"
Expand Down Expand Up @@ -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..."
Expand Down
89 changes: 15 additions & 74 deletions surfsense_web/app/(home)/downloads/download-panels.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p className="ss-home-body mt-8 text-sm">
The desktop app is not available on {os}. Browse{" "}
<a className="ss-home-link" href={GITHUB_RELEASES_URL}>
all releases
</a>{" "}
instead.
</p>
);
}

if (isLoading) {
return (
<FlowButton
className="mt-8 opacity-50 pointer-events-none"
text={`Download for ${os}`}
disabled
/>
);
}

return (
<FlowButton
className="mt-8"
href={primary?.url ?? GITHUB_RELEASES_URL}
text={`Download for ${os}`}
/>
);
}

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[] = [
Expand Down Expand Up @@ -84,16 +42,10 @@ export function AllReleasesLink() {
);
}

export function OSDownloadGrid() {
const { assets, isLoading } = useLatestRelease();

export function OSDownloadGrid({ assets }: { assets: ReleaseAsset[] }) {
return (
<div className="ss-home-grid ss-home-grid-3 ss-home-grid-dashed">
{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(
Expand All @@ -105,23 +57,12 @@ export function OSDownloadGrid() {
<div key={panel.title} className="ss-home-cell flex flex-col">
<h3 className="ss-home-h3">{panel.title}</h3>
<div className="mt-4 flex flex-col items-start gap-2">
{isLoading
? panel.suffixes.map((suffix) => (
<span
key={suffix}
aria-disabled="true"
className="ss-home-forward pointer-events-none opacity-50"
>
{ASSET_LABELS[suffix]}
<DownloadIcon aria-hidden="true" className="size-3.5" />
</span>
))
: panelAssets.map((asset) => (
<a key={asset.name} className="ss-home-forward" href={asset.url}>
{getAssetLabel(asset.name)}
<DownloadIcon aria-hidden="true" className="size-3.5" />
</a>
))}
{panelAssets.map((asset) => (
<a key={asset.name} className="ss-home-forward" href={asset.url}>
{getAssetLabel(asset.name)}
<DownloadIcon aria-hidden="true" className="size-3.5" />
</a>
))}
</div>
</div>
);
Expand Down
34 changes: 16 additions & 18 deletions surfsense_web/app/(home)/downloads/page.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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 (
<>
<section className="ss-home-hero ss-home-pad">
Expand All @@ -30,20 +28,20 @@ export default function DownloadsPage() {
<p className="ss-home-lede mx-auto mt-6 max-w-xl">
One installer, no account, no cloud. Pick your platform below.
</p>
<div className="mt-2 flex justify-center">
<PrimaryDownloadButton />
</div>
<TrialForm note="We will send a 30-day licence for the scraper plugins, with the download links. The installers are below either way." />
</div>
</section>

<section className="ss-home-rule ss-home-rule-plain">
<div className="ss-home-head ss-home-head-plain ss-home-head-tight">
<p className="ss-home-eyebrow">Choose your platform</p>
<h2 className="ss-home-h2 mt-2">Windows, macOS and Linux</h2>
</div>
{assets.length > 0 ? (
<section className="ss-home-rule ss-home-rule-plain">
<div className="ss-home-head ss-home-head-plain ss-home-head-tight">
<p className="ss-home-eyebrow">Choose your platform</p>
<h2 className="ss-home-h2 mt-2">Windows, macOS and Linux</h2>
</div>

<OSDownloadGrid />
</section>
<OSDownloadGrid assets={assets} />
</section>
) : null}

<section className="ss-home-rule">
<div className="ss-home-pad py-10 text-center">
Expand Down
14 changes: 8 additions & 6 deletions surfsense_web/app/(home)/license/license-forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -251,7 +253,7 @@ export function TrialForm() {
}

return (
<div className="mt-10 flex flex-col items-center gap-3">
<div className="mt-10 flex flex-col items-center gap-5">
<form onSubmit={handleSubmit} className="flex items-center gap-2">
<InlineEmailField id={`trial-email-${id}`} value={email} onChange={setEmail} />
<HomeButton type="submit" size="xl" disabled={busy} className="relative shrink-0">
Expand All @@ -261,12 +263,12 @@ export function TrialForm() {
</form>
<p
className={
outcome?.kind === "error" ? "text-sm text-destructive" : "ss-home-body text-sm"
outcome?.kind === "error"
? "max-w-sm text-sm text-destructive"
: "ss-home-body max-w-sm text-sm"
}
>
{outcome
? outcome.message
: "One trial per email address. We will send the license file to your inbox."}
{outcome ? outcome.message : note}
</p>
</div>
);
Expand Down
5 changes: 5 additions & 0 deletions surfsense_web/app/(home)/sunset/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { RuntimeConfig } from "@/components/providers/runtime-config.server";

export default function SunsetLayout({ children }: { children: React.ReactNode }) {
return <RuntimeConfig>{children}</RuntimeConfig>;
}
19 changes: 17 additions & 2 deletions surfsense_web/app/(home)/sunset/sunset-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down Expand Up @@ -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
Expand All @@ -54,7 +69,7 @@ export function SunsetExport() {
async function handleExport() {
if (isExporting) return;
if (session.status !== "authenticated") {
redirectToLogin();
signIn();
return;
}

Expand All @@ -65,7 +80,7 @@ export function SunsetExport() {
skipAuthRedirect: true,
});
if (response.status === 401) {
redirectToLogin();
signIn();
return;
}
if (!response.ok) {
Expand Down
2 changes: 1 addition & 1 deletion surfsense_web/components/pricing/pricing-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
2 changes: 0 additions & 2 deletions surfsense_web/components/site/site-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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 },
],
},
];
10 changes: 1 addition & 9 deletions surfsense_web/components/site/site-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -149,13 +148,6 @@ export function SiteNav({ starCount, starsHref }: { starCount: number | null; st
<ThemeTogglerComponent />
</span>

{/* 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. */}
<HomeButton asChild>
<Link href={SIGN_IN_URL}>Sign in</Link>
</HomeButton>

<button
type="button"
onClick={() => {
Expand Down
38 changes: 38 additions & 0 deletions surfsense_web/lib/app-release.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* The desktop release this site links to, and how its assets are named.
*
* `APP_RELEASE_VERSION` is written by `surfsense_local/scripts/bump-version.sh`.
* It lives here because the web image builds with `context: ./surfsense_web`,
* so nothing outside this directory exists at build time.
*
* No React and no directives, so server and client code can both import it.
*/

export const APP_RELEASE_VERSION = "2.0.0";

export const APP_RELEASE_TAG = `v${APP_RELEASE_VERSION}`;

/** The list, not `/releases/latest`, which is pinned to the legacy 0.0.x app. */
export const GITHUB_RELEASES_URL = "https://github.com/MODSetter/SurfSense/releases";

export interface ReleaseAsset {
name: string;
url: string;
}

export const ASSET_LABELS: Record<string, string> = {
".exe": "Windows (exe)",
"-arm64.dmg": "macOS Apple Silicon (dmg)",
"-x64.dmg": "macOS Intel (dmg)",
"-arm64.zip": "macOS Apple Silicon (zip)",
"-x64.zip": "macOS Intel (zip)",
".AppImage": "Linux (AppImage)",
".deb": "Linux (deb)",
};

export function getAssetLabel(name: string): string {
for (const [suffix, label] of Object.entries(ASSET_LABELS)) {
if (name.endsWith(suffix)) return label;
}
return name;
}
Loading
Loading