diff --git a/src/app/[org]/env-new/page.tsx b/src/app/[org]/env-new/page.tsx index b77f3519..6a97f21c 100644 --- a/src/app/[org]/env-new/page.tsx +++ b/src/app/[org]/env-new/page.tsx @@ -1,25 +1,14 @@ -"use client"; - -import { EnvironmentCreationForm } from "@/components/env/env-creation-form"; -import { authClient } from "@/lib/auth-client"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - +import { TakingABreak } from "@/components/taking-a-break"; + +/** + * humanlog.io is taking a break, so no new environments — creating one used to + * open a Stripe subscription (OrganizationService.CreateEnvironment). The + * backend refuses that RPC independently; this is the front door only. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function NewEnvironmentPage() { - const router = useRouter(); - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - useEffect(() => { - if (!activeOrganization) return; - const url = `/${activeOrganization.slug}/env-new`; - router.replace(url); - }, [activeOrganization]); - return ( -
-

Create a new environment

- -
+ ); } diff --git a/src/app/create-org/page.tsx b/src/app/create-org/page.tsx index 4e4446ac..2e50b96c 100644 --- a/src/app/create-org/page.tsx +++ b/src/app/create-org/page.tsx @@ -1,23 +1,12 @@ -"use client"; - -import { CreateOrg } from "@/components/organization/create-org"; -import { toast } from "sonner"; -import { useRouter } from "next/navigation"; -import { authClient } from "@/lib/auth-client"; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so organization creation is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function CreateOrgPage() { - const router = useRouter(); - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - const onSuccess = () => { - toast.success("Organization created successfully"); - router.push(`/${activeOrganization?.slug}/localhost/query`); - }; - return ( -
- -
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2faf2404..acb3809d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -13,6 +13,7 @@ import { OTELProvider } from "@/context/otel-provider"; import { PostHogProvider } from "@/context/posthog-provider"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Header } from "@/components/header"; +import { BreakBanner } from "@/components/break-banner"; import { Suspense } from "react"; export const metadata: Metadata = { @@ -51,9 +52,10 @@ export default function RootLayout({
-
- {children} +
+
+
{children}
diff --git a/src/app/onboarding/create-org/page.tsx b/src/app/onboarding/create-org/page.tsx index ea19fd45..11a9ec48 100644 --- a/src/app/onboarding/create-org/page.tsx +++ b/src/app/onboarding/create-org/page.tsx @@ -1,25 +1,12 @@ -"use client"; - -import { toast } from "sonner"; -import { useRouter } from "next/navigation"; -import { CreateOrg } from "@/components/organization/create-org"; -import { authClient } from "@/lib/auth-client"; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so onboarding is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ export default function OnboardingCreateOrgPage() { - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - const router = useRouter(); - - const onSuccess = () => { - toast.success("Organization created successfully"); - router.push(`/${activeOrganization?.slug}/localhost/query`); - - // TODO - // router.push("/onboarding/pricing"); - }; return ( -
- -
+ ); } diff --git a/src/app/onboarding/pricing/page.tsx b/src/app/onboarding/pricing/page.tsx index 6be04a64..dd098be6 100644 --- a/src/app/onboarding/pricing/page.tsx +++ b/src/app/onboarding/pricing/page.tsx @@ -1,15 +1,12 @@ -"use client"; +import { TakingABreak } from "@/components/taking-a-break"; -import OnboardingPricing from "@/components/onboarding/pricing"; - -export default function OnboardingPage() { +/** + * humanlog.io is taking a break, so there is no plan to pick. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function OnboardingPricingPage() { return ( -
-
- -
- -
-
+ ); } diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index a31711c0..e60b1454 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,17 +1,14 @@ -"use client"; - -import { useQuery } from "@connectrpc/connect-query"; -import { listProduct } from "api/js/svc/product/v1/service-ProductService_connectquery"; -import Pricing from "@/components/pricing"; - -export default function Page() { - const { isLoading, data } = useQuery(listProduct, { category: "logging" }); +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so there are no plans to show and no checkout + * to enter. The pricing components are left in the tree for whenever the lights + * come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function PricingPage() { return ( - + ); } diff --git a/src/app/sign-in/layout.tsx b/src/app/sign-in/layout.tsx index 155d028e..be622355 100644 --- a/src/app/sign-in/layout.tsx +++ b/src/app/sign-in/layout.tsx @@ -1,38 +1,16 @@ -"use client"; - -import { useFeatureFlag } from "@/hooks/useFeatureFlag"; -import { ChevronLeft } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -export default function SignInLayout({ - children, -}: { - children: React.ReactNode; -}) { - const router = useRouter(); - - const { flagEnabled: isAuthMigrationReady, isLoading } = - useFeatureFlag("release_auth_temp"); - - useEffect(() => { - if (!isLoading && !isAuthMigrationReady) { - router.replace("/"); - } - }, [isLoading, isAuthMigrationReady, router]); - - if (isLoading || !isAuthMigrationReady) return null; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so there is nothing to sign in to. + * + * The notice replaces `children` entirely rather than gating it, so every route + * under /sign-in (including the password-reset flow) lands on it. The + * underlying pages are left in place for whenever the lights come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function SignInLayout() { return ( -
- -
{children}
-
+ ); } diff --git a/src/app/sign-up/layout.tsx b/src/app/sign-up/layout.tsx index a732040c..1782283a 100644 --- a/src/app/sign-up/layout.tsx +++ b/src/app/sign-up/layout.tsx @@ -1,38 +1,16 @@ -"use client"; - -import { useFeatureFlag } from "@/hooks/useFeatureFlag"; -import { ChevronLeft } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; - -export default function SignUpLayout({ - children, -}: { - children: React.ReactNode; -}) { - const router = useRouter(); - - const { flagEnabled: isAuthMigrationReady, isLoading } = - useFeatureFlag("release_auth_temp"); - - useEffect(() => { - if (!isLoading && !isAuthMigrationReady) { - router.replace("/"); - } - }, [isLoading, isAuthMigrationReady, router]); - - if (isLoading || !isAuthMigrationReady) return null; +import { TakingABreak } from "@/components/taking-a-break"; +/** + * humanlog.io is taking a break, so we're not accepting new signups. + * + * The notice replaces `children` entirely rather than gating it, so every route + * under /sign-up lands on it. The underlying pages are left in place for + * whenever the lights come back on. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function SignUpLayout() { return ( -
- - {children} -
+ ); } diff --git a/src/app/user/purchase/page.tsx b/src/app/user/purchase/page.tsx index 2a1aa8d7..bafc924f 100644 --- a/src/app/user/purchase/page.tsx +++ b/src/app/user/purchase/page.tsx @@ -1,405 +1,13 @@ -"use client"; - -import { - ProductPane, - TotalPriceSummary, -} from "@/components/env/env-creation-form"; -import { Button } from "@/components/ui/button"; -import { Form, FormLabel } from "@/components/ui/form"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { cn } from "@/lib/utils"; -import { ConnectError } from "@connectrpc/connect"; -import { useForm } from "react-hook-form"; -import { useMutation, useQuery } from "@connectrpc/connect-query"; -import { - Elements, - PaymentElement, - useElements, - useStripe, -} from "@stripe/react-stripe-js"; -import { loadStripe, Stripe } from "@stripe/stripe-js"; -import { - createAddonSubscription, - createStripeCustomerSession, - getStripePublishableKey, -} from "api/js/svc/organization/v1/service-OrganizationService_connectquery"; -import { - CreateAddonSubscriptionRequest, - CreateAddonSubscriptionResponse, -} from "api/js/svc/organization/v1/service_pb"; -import { listProduct } from "api/js/svc/product/v1/service-ProductService_connectquery"; -import { Price as APIPrice } from "api/js/types/v1/price_pb"; -import { - Product as APIProduct, - Product_Scope, -} from "api/js/types/v1/product_pb"; -import { Loader } from "lucide-react"; -import Image from "next/image"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Dispatch, SetStateAction, useMemo, useState } from "react"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { getOrgEnvUrl } from "@/lib/utils/navigation"; -import { usePageStore } from "@/stores/page-store"; -import { useEnvironmentStore } from "@/stores/environment-store"; -import { authClient } from "@/lib/auth-client"; - -interface Product { - product: APIProduct; - prices: APIPrice[]; -} - -export default function UserAddonsPage() { - const searchParams = useSearchParams(); - const urlPlanId = searchParams.get("plan"); - const [isBilledYearly, setIsBilledYearly] = useState(true); - const [selectedProduct, setSelectedProduct] = useState(); - const [price, setPrice] = useState(); - const [stripePromise, setStripePromise] = useState< - Promise | undefined - >(); - - const stripeClientSecret = useQuery(createStripeCustomerSession).data - ?.customerSessionClientSecret; - const stripePK = useQuery(getStripePublishableKey).data?.stripePublishableKey; - - // fetch the product list, set the defaults - const { data } = useQuery(listProduct, { - category: "logging", - scope: Product_Scope.Organization, - }); - - const products = data?.items - .filter((el) => { - return ( - el.product?.scope == Product_Scope.Organization && el.prices.length > 0 - // && el.prices.filter((price) => Number(price.unitAmount) !== 0).length > 0 - ); - }) - .map((el): Product => { - return { product: el.product!, prices: el.prices }; - }); - - useMemo(() => { - if (selectedProduct || !data || !data.defaultProduct) { - return; - } - if (products?.length == 1) { - setSelectedProduct(products[0]); - return; - } - const defaultProduct = data.defaultProduct; - let item = data.items - .filter((el) => { - return el.product?.scope == Product_Scope.Organization; - }) - .find((el) => el.product?.stripeId === defaultProduct.stripeId); - item && setSelectedProduct({ product: item.product!, prices: item.prices }); - }, [products, data, selectedProduct]); - - useMemo(() => { - if (!selectedProduct) { - setPrice(data?.defaultProduct?.defaultPrice); - return; - } - - const selectedPrice = selectedProduct.prices.find((p) => { - if (isBilledYearly && p.recurring?.interval.includes("year")) { - return true; - } - if (!isBilledYearly && p.recurring?.interval.includes("month")) { - return true; - } - if (p.lookupKey === "free_personal_use") { - return true; - } - return false; - }); - - setPrice(selectedPrice); - }, [isBilledYearly, selectedProduct, data?.defaultProduct?.defaultPrice]); - - useMemo(() => { - if (selectedProduct || !data) return; - - const defaultProduct = urlPlanId - ? data.items.find((el) => el.product?.stripeId === urlPlanId) - : data.items.find( - (el) => el.product?.stripeId === data.defaultProduct?.stripeId, - ); - - defaultProduct && - setSelectedProduct({ - product: defaultProduct.product!, - prices: defaultProduct.prices, - }); - }, [data, selectedProduct, urlPlanId]); - - useMemo(() => { - if (!stripePK) { - return; - } - setStripePromise(loadStripe(stripePK)); - }, [stripePK]); - - if (!stripePromise || !price || !products) { - return ( -
- -
- ); - } - - return ( -
-

Subscribe

- - - -
- ); -} - -const formSchema = z.object({}); - -function CheckoutForm({ - selectedProduct, - setSelectedProduct, - price, - setPrice, - isBilledYearly, - setIsBilledYearly, - products, -}: { - selectedProduct: Product | undefined; - setSelectedProduct: Dispatch>; - price: APIPrice | undefined; - setPrice: Dispatch>; - isBilledYearly: boolean; - setIsBilledYearly: Dispatch>; - products: Product[]; -}) { - const router = useRouter(); - const stripe = useStripe(); - const elements = useElements(); - - const { useActiveOrganization } = authClient; - const { data: activeOrganization } = useActiveOrganization(); - - const { activeEnvironment } = useEnvironmentStore(); - const { activePage } = usePageStore(); - - const [errorMessage, setErrorMessage] = useState(); - - const handleError = (error: string) => { - setErrorMessage(error); - }; - - const { mutateAsync, isPending } = useMutation(createAddonSubscription); - - async function onSubmit(values: z.infer) { - if (!stripe || !elements || !price) { - return; - } - - const { error: submitError } = await elements.submit(); - if (submitError) { - console.log("submitError", submitError); - handleError(submitError.message!); - return; - } - let confirmationToken; - if (Number(price.unitAmount > 0)) { - const { error, confirmationToken: _confirmationToken } = - await stripe.createConfirmationToken({ - elements, - }); - - confirmationToken = _confirmationToken; - if (error) { - handleError(error.message!); - return; - } - } - - let res: CreateAddonSubscriptionResponse; - try { - res = await mutateAsync({ - payment: { - case: "stripe", - value: { - confirmationToken: confirmationToken?.id || "", - priceId: price?.stripeId, - }, - }, - }); - } catch (e) { - if (e instanceof ConnectError) { - handleError(e.message); - } else { - handleError("an unexpected problem occured"); - throw e; - } - return; - } - switch (res.payment.case) { - case "stripe": - if (res.payment.value.status === "requires_action") { - const { error } = await stripe.handleNextAction({ - clientSecret: res.payment.value.clientSecret, - }); - if (error) { - handleError(error.message!); - return; - } - } - } - console.log("checkout completed!"); - - if (!activeOrganization || !activeEnvironment) return; - - const url = getOrgEnvUrl(activeOrganization, activeEnvironment, activePage); - router.push(url); - } - - const form = useForm>({ - resolver: zodResolver(formSchema), - }); - +import { TakingABreak } from "@/components/taking-a-break"; + +/** + * humanlog.io is taking a break. This used to be the live Stripe Elements + * checkout; it must not load payment UI while the service is closed. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export default function PurchasePage() { return ( -
- -
-
- Pick a product -
- - -
-
- - {/* Product Section */} - -
- {products.map((product) => { - const stripeID = product.product.stripeId; - const isSelected = selectedProduct?.product.stripeId === stripeID; - return ( - { - setSelectedProduct(product); - setPrice(price); - }} - /> - ); - })} -
-
- {/* Payment Section */} - {Number(price?.unitAmount) > 0 && ( -
- -
-
- Powered by Stripe - -
-
-
- )} - - {/* Submit Section */} -
- {price?.lookupKey == "free_personal_use" && ( - <> - -
- -
- - )} - {price?.lookupKey != "free_personal_use" && ( - <> - -
- -
- {/* Find the selected product */} - {price && ( - - )} -
- {errorMessage &&
{errorMessage}
} -
- - )} -
-
- + ); } diff --git a/src/components/break-banner.tsx b/src/components/break-banner.tsx new file mode 100644 index 00000000..fef03fe8 --- /dev/null +++ b/src/components/break-banner.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { DismissibleBanner } from "@/components/ui/dismissable-banner"; +import { RETRO_URL } from "@/components/taking-a-break"; + +/** + * Site-wide notice that humanlog.io is paused. + * + * See https://www.webscale.lol/blog/humanlog-retro + */ +export function BreakBanner() { + return ( + + ); +} + +export default BreakBanner; diff --git a/src/components/header/user-actions.tsx b/src/components/header/user-actions.tsx index 2f78c421..4599118b 100644 --- a/src/components/header/user-actions.tsx +++ b/src/components/header/user-actions.tsx @@ -15,7 +15,6 @@ import Link from "next/link"; import { ThemeToggle } from "@/components/theme-toggle"; import { User as BetterAuthUser } from "better-auth"; import { authClient } from "@/lib/auth-client"; -import { Button } from "@/components/ui/button"; import { NavigationMenuItem } from "@/components/ui/navigation-menu"; import { useEnvironmentStore } from "@/stores/environment-store"; import { usePageStore } from "@/stores/page-store"; @@ -48,19 +47,14 @@ export const UserActions = ({ user }: UserActionsProps) => { }); }; + // humanlog.io is taking a break, so there's nothing to sign in to and the + // "Sign In" button is gone. Signed-out visitors keep the theme toggle, which + // otherwise only exists inside the signed-in menu below. + // See https://www.webscale.lol/blog/humanlog-retro return !user ? ( - <> - - - - + + + ) : ( diff --git a/src/components/landing-page/hero-marketing.tsx b/src/components/landing-page/hero-marketing.tsx index 53889854..ddc2219b 100644 --- a/src/components/landing-page/hero-marketing.tsx +++ b/src/components/landing-page/hero-marketing.tsx @@ -281,7 +281,7 @@ const AboveFoldHero: React.FC = () => { {/* CTA Buttons */}
- + { return (

- Ready to Get Started? + Run it on your laptop

@@ -22,17 +21,18 @@ const CTABannerSection: React.FC = () => {
- -
-

- Need a hosted solution for your team? -

- -
-
+ {/* + The hosted service is on a break, so this is no longer gated behind + the `experiment_cloud_waitlist_temp` flag — everyone should see where + the hosted tool went. + See https://www.webscale.lol/blog/humanlog-retro + */} +
+

+ Looking for the hosted version? +

+ +
); diff --git a/src/components/landing-page/sections/faq-section.tsx b/src/components/landing-page/sections/faq-section.tsx index c72e175d..3f7434a4 100644 --- a/src/components/landing-page/sections/faq-section.tsx +++ b/src/components/landing-page/sections/faq-section.tsx @@ -158,14 +158,11 @@ const faqItems: FAQItem[] = [ answer: ( <>

- Yes! We{"'"}re working on a hosted solution for teams that need - managed infrastructure. + Not right now. humanlog.io is taking a break — the hosted tool is + moving to a new project called minitape, with no timeline promised.

- +
), diff --git a/src/components/landing-page/shared/cloud-beta-signup.tsx b/src/components/landing-page/shared/cloud-beta-signup.tsx index dfc7a503..6cab0910 100644 --- a/src/components/landing-page/shared/cloud-beta-signup.tsx +++ b/src/components/landing-page/shared/cloud-beta-signup.tsx @@ -18,7 +18,7 @@ interface CloudBetaSignupProps { } const CloudBetaSignup: React.FC = ({ - heading = "Signup for our private beta of Humanlog Cloud. Reuse everything you learn locally, skills transfer 1:1, spots limited to 50 engineers.", + heading = "humanlog.io is taking a break. The hosted tool is moving to a new project called minitape — leave your email to hear when it launches.", className = "", variant = "default", }) => { diff --git a/src/components/landing-page/shared/install-cta.tsx b/src/components/landing-page/shared/install-cta.tsx index b2d0d959..69db6269 100644 --- a/src/components/landing-page/shared/install-cta.tsx +++ b/src/components/landing-page/shared/install-cta.tsx @@ -35,7 +35,7 @@ interface InstallCTAProps { } const InstallCTA: React.FC = ({ - buttonText = "Install & Sign Up", + buttonText = "Install humanlog", buttonSize = "lg", buttonVariant = "default", className = "h-11", diff --git a/src/components/taking-a-break.tsx b/src/components/taking-a-break.tsx new file mode 100644 index 00000000..22ee7713 --- /dev/null +++ b/src/components/taking-a-break.tsx @@ -0,0 +1,72 @@ +"use client"; + +import React from "react"; +import { ArrowUpRight } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import CloudBetaSignup from "@/components/landing-page/shared/cloud-beta-signup"; +import { cn } from "@/lib/utils"; + +export const RETRO_URL = "https://www.webscale.lol/blog/humanlog-retro"; +export const CONTACT_EMAIL = "antoine@webscale.lol"; + +interface TakingABreakProps { + className?: string; + /** Shown under the heading. Defaults to the general "we're on a break" copy. */ + description?: React.ReactNode; +} + +/** + * The site-wide "humanlog.io is taking a break" notice. + * + * Rendered in place of anything that used to sign someone up, log them in, or + * take their money. See https://www.webscale.lol/blog/humanlog-retro + */ +export function TakingABreak({ className, description }: TakingABreakProps) { + return ( +
+ ); +} + +export default TakingABreak; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 47f2f002..8589f962 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -30,6 +30,22 @@ const betterAuthCallbacksClient = createClient( ); const betterAuthAdapter = createBetterAuthAdapter(betterAuthAdapterClient); +const shared = createSharedBetterAuthConfig({ + callbacksClient: betterAuthCallbacksClient, + createInvitationUrl: (invitationId: string) => { + return `${getSelfURL()}/invitation-callback?invitationId=${invitationId}`; + }, + + github: { + clientId: process.env.GITHUB_CLIENT_ID || "", + clientSecret: process.env.GITHUB_CLIENT_SECRET || "", + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID || "", + clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", + }, +}); + const config = { database: betterAuthAdapter, secret: process.env.BETTER_AUTH_SECRET || "", @@ -41,21 +57,21 @@ const config = { }, // telemetry: { enabled: true, debug: true }, - ...createSharedBetterAuthConfig({ - callbacksClient: betterAuthCallbacksClient, - createInvitationUrl: (invitationId: string) => { - return `${getSelfURL()}/invitation-callback?invitationId=${invitationId}`; - }, + ...shared, - github: { - clientId: process.env.GITHUB_CLIENT_ID || "", - clientSecret: process.env.GITHUB_CLIENT_SECRET || "", - }, - google: { - clientId: process.env.GOOGLE_CLIENT_ID || "", - clientSecret: process.env.GOOGLE_CLIENT_SECRET || "", - }, - }), + // humanlog.io is taking a break: https://www.webscale.lol/blog/humanlog-retro + // + // These overrides must stay *after* the spread above, and must merge into the + // shared config rather than replace it, or the callbacks it carries go with + // them. Disabling email/password closes /api/auth/sign-in/email and + // /api/auth/sign-up/email; emptying socialProviders removes the GitHub and + // Google routes, which otherwise auto-create an account for any unknown + // identity that hits `signIn.social`. + // + // The backend refuses account and session creation independently — see the + // closed-for-break interceptor in apisvc. This is the front door only. + emailAndPassword: { ...shared.emailAndPassword, enabled: false }, + socialProviders: {}, }; export const auth = betterAuth(config);