diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f241cbd26..024d50df1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Test billing display + run: pnpm --filter @openwork-ee/den-web test:billing + - name: Build web run: pnpm --filter @openwork-ee/den-web build diff --git a/ee/apps/den-web/app/(den)/_components/auth-panel.tsx b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx index 493702824..eef166301 100644 --- a/ee/apps/den-web/app/(den)/_components/auth-panel.tsx +++ b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx @@ -124,7 +124,7 @@ export function AuthPanel({ const resolvedSignUpContent: PanelContent = { title: "Get started.", - copy: "Free to try. Team plans from $50/mo.", + copy: "Start with desktop, then add Cloud when your team needs shared setup.", submitLabel: "Create account", togglePrompt: "Have an account?", toggleActionLabel: "Sign in", @@ -217,7 +217,7 @@ export function AuthPanel({ ) : null} -

+

If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app.

diff --git a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx index 4bbb202ae..a070a3333 100644 --- a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx @@ -1,7 +1,5 @@ "use client"; -import { PaperMeshGradient } from "@openwork/ui/react"; -import { Dithering } from "@paper-design/shaders-react"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useRef } from "react"; import { isSamePathname } from "../_lib/client-route"; @@ -76,30 +74,10 @@ export function AuthScreen() {
-
- - - -
+
+
+
+
@@ -111,7 +89,7 @@ export function AuthScreen() { OpenWork Cloud -

+

One setup, every seat.

diff --git a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx index f0f4eed04..a559e572a 100644 --- a/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/checkout-screen.tsx @@ -1,37 +1,71 @@ "use client"; +import { + CheckCircle2, + CreditCard, + Download, + RefreshCw, + Server, + ShieldCheck, + Users, +} from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; +import { + buildLocalMockBillingSummary, + formatBillingPlanLabels, + getBillingStatusLabel, + getWorkspacePlanInlineEntitlementCopy, + getWorkspacePlanEntitlementCopy, + isLocalMockBillingEnabled, +} from "../_lib/billing-display"; import { isSamePathname } from "../_lib/client-route"; -import { formatMoneyMinor } from "../_lib/den-flow"; import { useDenFlow } from "../_providers/den-flow-provider"; // For local layout testing (no deploy needed) // Enable with: NEXT_PUBLIC_DEN_MOCK_BILLING=1 -const MOCK_BILLING = process.env.NEXT_PUBLIC_DEN_MOCK_BILLING === "1"; const MOCK_CHECKOUT_URL = (process.env.NEXT_PUBLIC_DEN_MOCK_CHECKOUT_URL ?? "").trim() || null; -function formatSubscriptionStatus(value: string | null | undefined) { - if (!value) return "Purchase required"; - return value - .split(/[_\s]+/) - .filter(Boolean) - .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1).toLowerCase()) - .join(" "); -} - function LoadingPanel({ title, body }: { title: string; body: string }) { return (

-

{title}

+

{title}

{body}

); } -export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: string | null }) { +function FeatureLine({ + icon: Icon, + title, + body, +}: { + icon: typeof CheckCircle2; + title: string; + body?: string; +}) { + return ( +
+
+
+
+

{title}

+ {body ?

{body}

: null} +
+
+ ); +} + +export function CheckoutScreen({ + customerSessionToken, + requestHost, +}: { + customerSessionToken: string | null; + requestHost: string | null; +}) { const router = useRouter(); const pathname = usePathname(); const handledReturnRef = useRef(false); @@ -52,22 +86,13 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: resolveUserLandingRoute, } = useDenFlow(); - const mockMode = MOCK_BILLING && process.env.NODE_ENV !== "production"; - - const billingSummary = MOCK_BILLING - ? { - featureGateEnabled: true, - hasActivePlan: false, - checkoutRequired: true, - checkoutUrl: MOCK_CHECKOUT_URL, - portalUrl: null, - price: { amount: 5000, currency: "usd", recurringInterval: "month", recurringIntervalCount: 1 }, - subscription: null, - invoices: [], - productId: null, - benefitId: null, - } - : realBillingSummary; + const mockMode = isLocalMockBillingEnabled({ + flag: process.env.NEXT_PUBLIC_DEN_MOCK_BILLING, + host: requestHost, + nodeEnv: process.env.NODE_ENV, + }); + + const billingSummary = mockMode ? buildLocalMockBillingSummary(MOCK_CHECKOUT_URL) : realBillingSummary; useEffect(() => { if (!sessionHydrated || resuming || user || mockMode) { @@ -182,53 +207,91 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: } const billingPrice = billingSummary?.price ?? null; - const showLoading = resuming || (billingBusy && !billingSummary && !MOCK_BILLING); - const checkoutHref = effectiveCheckoutUrl ?? MOCK_CHECKOUT_URL ?? null; - const planAmountLabel = - billingPrice && billingPrice.amount !== null - ? `${formatMoneyMinor(billingPrice.amount, billingPrice.currency)}/${billingPrice.recurringInterval}` - : "$50.00/month"; - const subscription = billingSummary?.subscription ?? null; - const subscriptionStatus = formatSubscriptionStatus(subscription?.status); + const showLoading = resuming || (billingBusy && !billingSummary && !mockMode); + const checkoutHref = effectiveCheckoutUrl ?? billingSummary?.checkoutUrl ?? (mockMode ? MOCK_CHECKOUT_URL : null); + const planLabels = formatBillingPlanLabels(billingPrice); + const planPriceLabel = planLabels.inline; + const hasActivePlan = Boolean(billingSummary?.hasActivePlan); + const subscriptionStatus = getBillingStatusLabel(billingSummary); return (
-
-
-
+
+
+

OpenWork Cloud

Purchase a plan before creating your workspace.

- Start with one workspace plan for $50/month. Each plan includes up to 5 members and 1 hosted worker. + Start with one workspace plan for {planPriceLabel}. Each plan {getWorkspacePlanInlineEntitlementCopy()}.

-
-
- {checkoutHref ? ( - - Purchase plan — $50/month +
+ {checkoutHref ? ( + + + ) : ( + + )} + + - ) : ( - - )} - - Use desktop only - +
+ +
+ {planLabels.available ? `${planLabels.amount} per workspace` : planLabels.inline} + {planLabels.available ? ( + <> + + {planLabels.cadence} + + ) : null} + + {user?.email ?? "Signed in"} +
-
- $50/month per workspace - - {planAmountLabel} billed monthly - - {user?.email ?? "Signed in"} +
+
+ + Workspace plan + +
+
+
+ {planLabels.amount} + {planLabels.cadence} +
+

+ {getWorkspacePlanEntitlementCopy()} +

+
+
+
+
+
+
+
+
+
@@ -239,10 +302,15 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: Refreshing access state...
) : null} + {!billingSummary && !showLoading ? ( +
+ Billing details are unavailable. Refresh the purchase link to retry. +
+ ) : null} {billingSummary ? ( -
-
+
+
OpenWork Cloud @@ -252,25 +320,19 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:

-
-
Share setup across your team and org
-
Background agents in alpha for selected workflows
-
Custom LLM providers with team access controls
-
- -
-
-

Background agents

-

- Keep selected workflows running in the background. Alpha. -

-
-
-

LLM providers

-

- Standardize provider access, model selection, and team rollout. -

-
+
+ + +
@@ -283,10 +345,19 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken:

-
-
Run locally for free
-
Keep data on your machine
-
Move into OpenWork Cloud later
+
+ + +
@@ -300,22 +371,22 @@ export function CheckoutScreen({ customerSessionToken }: { customerSessionToken: