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
18 changes: 12 additions & 6 deletions apps/desktop/src/components/license/activate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ export function LicenseKeyForm({
);
}

export function KeyDivider({ className }: { className?: string }) {
return (
<div className={cn("flex items-center gap-3 text-[11px] font-medium tracking-[0.12em] text-muted-foreground uppercase", className)}>
<span className="h-px flex-1 bg-border" />
Already have a key?
<span className="h-px flex-1 bg-border" />
</div>
);
}

/** Everything it takes to get a licence: the trial, the plans, a key, and help when the key is lost. */
export function ActivatePanel({
source,
Expand Down Expand Up @@ -143,14 +153,10 @@ export function ActivatePanel({
</Button>
</div>
<p className="mt-2.5 text-center text-xs text-muted-foreground">
{opened ? "Finish the checkout in your browser, then paste your licence key below." : "7 days free, then $39 a month. Cancel any time."}
{opened ? "Finish the checkout in your browser, then paste your licence key below." : "14 days free, then $39 a month. Cancel any time."}
</p>

<div className="my-6 flex items-center gap-3 text-[11px] font-medium tracking-[0.12em] text-muted-foreground uppercase">
<span className="h-px flex-1 bg-border" />
Already have a key?
<span className="h-px flex-1 bg-border" />
</div>
<KeyDivider className="my-6" />

<LicenseKeyForm onActivated={onActivated} autoFocus={autoFocus} />

Expand Down
36 changes: 27 additions & 9 deletions apps/desktop/src/components/license/license-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState, type ReactNode } from "react";
import { useNavigate } from "react-router";
import { useLocation, useNavigate } from "react-router";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { AnimatePresence, motion } from "motion/react";
import { differenceInCalendarDays, format } from "date-fns";
import { CreditCard, KeyRound, Sparkles, X } from "lucide-react";
import { CreditCard, Gift, KeyRound, Sparkles, X } from "lucide-react";
import type { LicenseState } from "@godmode/shared";
import { Button } from "@/components/ui/button";
import { api } from "@/lib/api";
Expand All @@ -12,6 +12,7 @@ import { openExternal } from "@/lib/desktop";
import { useLicense } from "@/lib/hooks";
import { qk } from "@/lib/queryKeys";
import { cn } from "@/lib/utils";
import { checkoutUrl } from "./activate";

const DAY_MS = 86_400_000;

Expand All @@ -22,7 +23,13 @@ interface Notice {
tone: "info" | "warning";
icon: ReactNode;
text: ReactNode;
action: { label: string; run: () => void } | null;
action: Action | null;
secondary?: Action;
}

interface Action {
label: string;
run: () => void;
}

const today = () => format(new Date(), "yyyy-MM-dd");
Expand All @@ -41,7 +48,7 @@ function inDays(iso: string): string {
return days <= 0 ? "today" : days === 1 ? "tomorrow" : `in ${days} days`;
}

function noticeFor(s: LicenseState, actions: { settings: () => void; manage: () => void; refresh: () => void }): Notice | null {
function noticeFor(s: LicenseState, actions: { settings: () => void; manage: () => void; refresh: () => void; trial: () => void }): Notice | null {
if (s.blocked) return null;
if (s.status === "trial" && s.trialEndsAt) {
const left = new Date(s.trialEndsAt).getTime() - Date.now();
Expand Down Expand Up @@ -72,9 +79,10 @@ function noticeFor(s: LicenseState, actions: { settings: () => void; manage: ()
return {
kind: "grace",
tone: "info",
icon: <KeyRound />,
text: <>Add your licence key by {format(new Date(s.graceEndsAt), "MMMM d")}. Until then everything works as before.</>,
action: { label: "Add key", run: actions.settings },
icon: <Gift />,
text: <>Free until {format(new Date(s.graceEndsAt), "MMMM d")}. Then add your licence key or start the 14-day free trial.</>,
action: { label: "Start free trial", run: actions.trial },
secondary: { label: "Add key", run: actions.settings },
};
}
if (s.status === "unverified" && s.unverifiedUntil) {
Expand All @@ -89,9 +97,10 @@ function noticeFor(s: LicenseState, actions: { settings: () => void; manage: ()
return null;
}

/** One slim line above the page when the licence needs attention soon; dismissed, it stays away for the day. */
/** One slim line above the page when the licence needs attention soon (not on the licence page); dismissed, it stays away for the day. */
export function LicenseBanner() {
const navigate = useNavigate();
const { pathname } = useLocation();
const qc = useQueryClient();
const { data } = useLicense();
const [, setDismissed] = useState(0);
Expand All @@ -101,9 +110,13 @@ export function LicenseBanner() {
settings: () => navigate("/settings/license"),
manage: () => data.manageUrl && void openExternal(data.manageUrl),
refresh: () => refresh.mutate(),
trial: () => {
navigate("/settings/license");
void openExternal(checkoutUrl("banner"));
},
})
: null;
const shown = notice && !dismissedToday(notice.kind) ? notice : null;
const shown = notice && !dismissedToday(notice.kind) && !pathname.startsWith("/settings/license") ? notice : null;

const dismiss = (kind: Kind) => {
try {
Expand Down Expand Up @@ -135,6 +148,11 @@ export function LicenseBanner() {
>
<span className={cn("shrink-0 [&_svg]:size-4", shown.tone === "warning" ? "text-warning" : "text-brand-strong")}>{shown.icon}</span>
<p className="min-w-0 flex-1 leading-snug">{shown.text}</p>
{shown.secondary && (
<Button size="sm" variant="ghost" className="h-7 shrink-0" onClick={shown.secondary.run}>
{shown.secondary.label}
</Button>
)}
{shown.action && (
<Button size="sm" variant="outline" className="h-7 shrink-0 bg-card" onClick={shown.action.run}>
{shown.action.label}
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/components/license/license-gate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export function LicenseGate({ state }: { state: LicenseState }) {
>
<Character character={MASCOT_CHARACTER} color={MASCOT_COLOR} size={76} mood="idle" follow title="Godmode" />
<p className="eyebrow mt-7">Godmode Pro</p>
<h1 className="heading-display mt-2 text-[32px] sm:text-[36px]">Activate Godmode</h1>
<h1 className="heading-display mt-2 text-[32px] sm:text-[36px]">
{state.status === "missing" && state.graceEndsAt ? "Your free week is over" : "Activate Godmode"}
</h1>
<p className="mt-3 max-w-sm text-[15px] leading-relaxed text-balance text-muted-foreground">
{state.message ?? "Start your free trial, or enter your licence key to keep your agents working."}
</p>
Expand Down
38 changes: 34 additions & 4 deletions apps/desktop/src/components/onboarding/activate-step.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
import { format } from "date-fns";
import { CircleCheck } from "lucide-react";
import { CircleCheck, Gift } from "lucide-react";
import type { LicenseState } from "@godmode/shared";
import { ActivatePanel } from "@/components/license/activate";
import { ActivatePanel, KeyDivider, LicenseKeyForm } from "@/components/license/activate";
import { Button } from "@/components/ui/button";
import { useLicense } from "@/lib/hooks";
import { SubmitButton } from "./auth-layout";
import { StepCard, StepFooter, StepHeader } from "./step-kit";

const READY: LicenseState["status"][] = ["active", "trial", "past_due", "unverified"];

/** Start the trial or enter a key. Skippable while nothing is refused yet (grace, builds from source). */
/** Start the trial or enter a key. Skippable while nothing is refused yet (free week, builds from source). */
export function ActivateStep({ onBack, onDone }: { onBack: () => void; onDone: (state: LicenseState | null) => void }) {
const { data: state } = useLicense();
const ready = !!state && !!state.keyHint && READY.includes(state.status) && !state.blocked;
const skippable = !!state && !state.blocked;
const freeUntil = state?.enforced && state.status === "grace" && state.graceEndsAt ? new Date(state.graceEndsAt) : null;

if (freeUntil) {
return (
<div>
<StepHeader
eyebrow="Godmode Pro"
title="Your free week starts now."
description="Every feature, no card needed. When the week is up, add your licence key or start the 14-day free trial."
/>
<StepCard>
<div className="flex items-center gap-3 rounded-lg border border-brand/25 bg-brand-soft p-4 text-sm">
<Gift className="size-5 shrink-0 text-brand-strong" />
<span>
Free until <span className="font-medium">{format(freeUntil, "EEEE, MMMM d")}</span>.
</span>
</div>

<KeyDivider className="my-6" />
<LicenseKeyForm onActivated={(next) => onDone(next)} />

<StepFooter onBack={onBack}>
<SubmitButton busy={false} type="button" onClick={() => onDone(state ?? null)} className="w-auto px-6">
Continue
</SubmitButton>
</StepFooter>
</StepCard>
</div>
);
}

return (
<div>
Expand Down Expand Up @@ -49,7 +79,7 @@ export function ActivateStep({ onBack, onDone }: { onBack: () => void; onDone: (
) : (
skippable && (
<Button type="button" variant="ghost" onClick={() => onDone(state ?? null)}>
{state?.status === "grace" && state.graceEndsAt ? `Later — until ${format(new Date(state.graceEndsAt), "MMM d")}` : "Skip for now"}
Skip for now
</Button>
)
)}
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/components/onboarding/done-step.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { format } from "date-fns";
import { motion } from "motion/react";
import { ArrowRight, Bot, CircleCheck, CircleDashed, Plug, ShieldCheck } from "lucide-react";
import { MASCOT_CHARACTER, MASCOT_COLOR, type LicenseState } from "@godmode/shared";
Expand Down Expand Up @@ -62,10 +63,11 @@ export function DoneStep({
];
const license = summary.license;
const licensed = !!license?.keyHint && !license.blocked && ["active", "trial", "past_due", "unverified"].includes(license.status);
const freeUntil = license?.enforced && license.status === "grace" && license.graceEndsAt ? format(new Date(license.graceEndsAt), "MMM d") : null;
items.push({
label: "Godmode Pro",
ok: licensed ? true : license?.blocked ? false : null,
detail: licensed ? (license!.status === "trial" ? "Free trial active" : "Activated") : license?.status === "grace" ? "Add your key in Settings → License" : "Skipped",
ok: licensed || freeUntil ? true : license?.blocked ? false : null,
detail: licensed ? (license!.status === "trial" ? "Free trial active" : "Activated") : freeUntil ? `Free until ${freeUntil}` : "Skipped",
});
if (serverMode)
items.push({
Expand Down
20 changes: 16 additions & 4 deletions apps/desktop/src/components/settings/license-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { format, formatDistanceToNow } from "date-fns";
import { differenceInCalendarDays, format, formatDistanceToNow } from "date-fns";
import { ExternalLink, KeyRound, RefreshCw, Sparkles } from "lucide-react";
import { toast } from "sonner";
import type { LicensePlan, LicenseState, LicenseStatus } from "@godmode/shared";
Expand Down Expand Up @@ -31,7 +31,7 @@ const STATUS: Record<LicenseStatus, { label: string; tone: "live" | "neutral" |
active: { label: "Active", tone: "live" },
trial: { label: "Free trial", tone: "live" },
past_due: { label: "Payment due", tone: "warning" },
grace: { label: "Key needed", tone: "warning" },
grace: { label: "Free week", tone: "live" },
unverified: { label: "Not checked yet", tone: "warning" },
missing: { label: "No licence", tone: "neutral" },
invalid: { label: "Invalid key", tone: "danger" },
Expand All @@ -46,6 +46,11 @@ const PLAN: Record<LicensePlan, string> = {

const day = (iso: string) => format(new Date(iso), "MMMM d, yyyy");

function daysLeft(iso: string): string {
const days = differenceInCalendarDays(new Date(iso), new Date());
return days <= 0 ? "ends today" : days === 1 ? "1 day left" : `${days} days left`;
}

export function LicenseSection() {
const license = useLicense();
return (
Expand Down Expand Up @@ -98,15 +103,22 @@ function LicenseCard({ state: s }: { state: LicenseState }) {
});

if (!s.keyHint) {
const freeWeek = s.status === "grace" && s.graceEndsAt ? s.graceEndsAt : null;
return (
<>
{s.message && <Callout tone={s.blocked ? "warning" : "info"}>{s.message}</Callout>}
{s.message && !freeWeek && <Callout tone={s.blocked ? "warning" : "info"}>{s.message}</Callout>}
<SettingsGroup
title="Activate Godmode"
icon={<Sparkles />}
description="Every feature in every plan. Your agents, chats and logins stay on this computer."
actions={<ToneBadge tone={s.status === "grace" ? "warning" : "neutral"}>{STATUS[s.status].label}</ToneBadge>}
actions={<ToneBadge tone={status.tone}>{status.label}</ToneBadge>}
>
{freeWeek && (
<InfoRow label="Free until">
{day(freeWeek)}
<span className="text-muted-foreground"> · {daysLeft(freeWeek)}</span>
</InfoRow>
)}
<div className="py-5">{local ? <ActivatePanel source="settings" /> : <Callout>Add the licence key in Godmode on the computer itself.</Callout>}</div>
</SettingsGroup>
{!s.enforced && <NotEnforced />}
Expand Down
1 change: 1 addition & 0 deletions apps/website/src/components/StickyCta.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { COUPON, PRICING } from '@/config/site';
<span class="min-w-0 flex-1 truncate text-[13.5px] whitespace-nowrap text-text-2 sm:flex-none">
<span class="text-text">{PRICING.trialDays} days free</span><span class="hidden min-[400px]:inline"> · then ${PRICING.plans.monthly.price}/mo</span><span class="hidden sm:inline"> · code <b class="font-mono text-brand-2">{COUPON.code}</b> −{COUPON.percentOff}% ends in </span><Countdown class="hidden sm:inline-flex" />
</span>
<a href="/download" class="btn btn-ghost btn-sm hidden lg:inline-flex" data-track="sticky_download">Try without a card</a>
<CheckoutButton plan="monthly" label="Start free trial" source="sticky" size="sm" />
</div>
</div>
6 changes: 3 additions & 3 deletions apps/website/src/components/sections/Faq.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const faqs: { key: string; q: string; a: string }[] = [
},
{
key: 'trial',
q: 'How does the free trial work?',
a: `Pick monthly or yearly and start your ${PRICING.trialDays}-day trial with a card, Apple Pay or Google Pay. Nothing is charged today. Cancel in the billing portal (one click from the app or your welcome page) before the ${PRICING.trialDays} days are up and you pay nothing. Otherwise your plan starts automatically when the trial ends: $${monthly.price} a month or $${yearly.price} a year, renewing until you cancel.`,
q: 'Can I try it for free?',
a: `Yes, two ways. Download Godmode and use it free for 7 days: no card, no key, every feature. Or pick monthly or yearly and start your ${PRICING.trialDays}-day trial with a card, Apple Pay or Google Pay. Nothing is charged today. Cancel in the billing portal (one click from the app or your welcome page) before the ${PRICING.trialDays} days are up and you pay nothing. Otherwise your plan starts automatically when the trial ends: $${monthly.price} a month or $${yearly.price} a year, renewing until you cancel. Either way, everything you set up stays when you add your licence key.`,
},
{
key: 'claude',
Expand Down Expand Up @@ -68,7 +68,7 @@ const faqs: { key: string; q: string; a: string }[] = [
{
key: 'free',
q: 'Godmode is open source. Why pay?',
a: `You don’t have to, if you can build software yourself. The whole app is on GitHub under the MIT License, free to build and change. Godmode Pro is for everyone else: the ready-to-install apps for Mac, Windows and Linux, automatic updates, the always-on version, and help from the people who build it. Your subscription also pays for Godmode’s development.`,
a: `You don’t have to, if you can build software yourself. The whole app is on GitHub under the MIT License, free to build and change, and your own build never asks for a key. Godmode Pro is for everyone else: the ready-to-install apps for Mac, Windows and Linux (free for 7 days, then with a Pro key), automatic updates, the always-on version, and help from the people who build it. Your subscription also pays for Godmode’s development.`,
},
{
key: 'cloud',
Expand Down
7 changes: 7 additions & 0 deletions apps/website/src/components/sections/FinalCta.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import { COUPON, FOUNDING, PRICING, yearlySavings } from '@/config/site';
<CheckoutButton plan="monthly" label={`Start ${PRICING.trialDays}-day free trial`} source="final" size="lg" />
<CheckoutButton plan="yearly" label={`Or yearly — $${PRICING.plans.yearly.perMonth}/mo, save ${yearlySavings}%`} source="final" variant="ghost" size="lg" />
</div>
<div class="mt-5" data-reveal style="--d:500">
<a href="/download" class="group inline-flex items-center gap-2 text-[14px] text-text-2 transition-colors hover:text-text" data-track="final_download">
<Icon name="download" size={15} class="text-brand" />
No card? Download it and use it free for 7 days
<Icon name="arrow-right" size={14} class="text-dim transition-transform duration-500 group-hover:translate-x-0.5" />
</a>
</div>
<p class="mt-6 flex items-center justify-center gap-2 text-[13px] text-muted" data-reveal style="--d:550">
<Icon name="shield-check" size={14} class="text-brand" />
$0 today · price locked for life · founding offer ends in <Countdown />
Expand Down
9 changes: 8 additions & 1 deletion apps/website/src/components/sections/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ import recording from '@/assets/shots/hero-recording.jpg';
Watch it work
</button>
</div>
<div class="mt-5" data-reveal style="--d:660">
<a href="/download" class="group inline-flex items-center gap-2 text-[14px] text-text-2 transition-colors hover:text-text" data-track="hero_download">
<Icon name="download" size={15} class="text-brand" />
No card? Download it and use it free for 7 days
<Icon name="arrow-right" size={14} class="text-dim transition-transform duration-500 group-hover:translate-x-0.5" />
</a>
</div>

<ul class="mt-6 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-[13px] text-muted" data-reveal style="--d:720">
<li class="flex items-center gap-1.5"><Icon name="check" size={14} class="text-brand" /> Founder sets up your first {FOUNDING.setupAutomations} automations</li>
Expand All @@ -62,7 +69,7 @@ import recording from '@/assets/shots/hero-recording.jpg';
</ul>

<p class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 text-[13px] text-muted" data-reveal style="--d:760">
<span class="flex items-center gap-1.5"><Icon name="laptop" size={14} class="text-dim" /> Mac and Windows</span>
<span class="flex items-center gap-1.5"><Icon name="laptop" size={14} class="text-dim" /> Mac, Windows and Linux</span>
<span class="flex items-center gap-1.5"><Icon name="message-circle" size={14} class="text-dim" /> No coding, just plain words</span>
<span class="flex items-center gap-1.5"><Icon name="lock-keyhole" size={14} class="text-dim" /> Your passwords stay on your computer</span>
</p>
Expand Down
10 changes: 10 additions & 0 deletions apps/website/src/components/sections/Pricing.astro
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@ const guarantees = [
}
</div>

<div class="mx-auto mt-4 flex max-w-[980px] flex-col gap-4 rounded-[22px] p-5 ring-1 ring-line sm:flex-row sm:items-center sm:justify-between sm:px-7" data-reveal style="--d:110">
<p class="flex items-start gap-3 text-[14px] leading-relaxed text-muted">
<Icon name="download" size={18} class="mt-0.5 flex-none text-brand" />
<span><span class="text-text">Not ready for a card?</span> Download Godmode and use it free for 7 days. Add a licence key when you’re ready, and everything you set up stays.</span>
</p>
<a href="/download" class="btn btn-ghost btn-sm flex-none self-start sm:self-auto" data-track="pricing_download">
Download free <Icon name="arrow-right" size={14} class="arrow" />
</a>
</div>

<div class="included mx-auto mt-4 max-w-[980px] overflow-hidden rounded-[26px] ring-1 ring-line" data-reveal style="--d:120">
<div class="flex items-center justify-between gap-4 px-6 pt-6 sm:px-8 sm:pt-8">
<p class="text-[13px] text-muted">Everything a founding seat gets you</p>
Expand Down
Loading
Loading