- {/* Background layers: mesh gradient wrapped in a dithering texture */}
-
+
);
diff --git a/ee/apps/den-web/app/(den)/_lib/billing-display.test.ts b/ee/apps/den-web/app/(den)/_lib/billing-display.test.ts
new file mode 100644
index 000000000..a7f4cb60b
--- /dev/null
+++ b/ee/apps/den-web/app/(den)/_lib/billing-display.test.ts
@@ -0,0 +1,160 @@
+import assert from "node:assert/strict";
+import { describe, test } from "node:test";
+import {
+ buildLocalMockBillingSummary,
+ formatBillingAmountLabel,
+ formatBillingPlanLabels,
+ formatBillingStatusLabel,
+ getBillingStatusLabel,
+ getWorkspacePlanEntitlementCopy,
+ getWorkspacePlanInlineEntitlementCopy,
+ getWorkspacePlanShortEntitlementCopy,
+ isLocalMockBillingEnabled,
+ isProductionBillingHost,
+} from "./billing-display";
+
+describe("billing display helpers", () => {
+ test("never enables local mock billing in production", () => {
+ assert.equal(isLocalMockBillingEnabled({ flag: "1", nodeEnv: "production" }), false);
+ });
+
+ test("enables local mock billing only when explicitly requested outside production", () => {
+ assert.equal(isLocalMockBillingEnabled({ flag: "1", nodeEnv: "development" }), true);
+ assert.equal(isLocalMockBillingEnabled({ flag: undefined, nodeEnv: "development" }), false);
+ assert.equal(isLocalMockBillingEnabled({ flag: "0", nodeEnv: "development" }), false);
+ });
+
+ test("never enables local mock billing on the production billing host", () => {
+ assert.equal(
+ isLocalMockBillingEnabled({
+ flag: "1",
+ host: "app.openworklabs.com",
+ nodeEnv: "development",
+ }),
+ false,
+ );
+ assert.equal(
+ isLocalMockBillingEnabled({
+ flag: "1",
+ host: "APP.OPENWORKLABS.COM:443, internal-proxy",
+ nodeEnv: "development",
+ }),
+ false,
+ );
+ assert.equal(
+ isLocalMockBillingEnabled({
+ flag: "1",
+ host: "localhost:3005",
+ nodeEnv: "development",
+ }),
+ true,
+ );
+ });
+
+ test("detects production billing hosts consistently", () => {
+ assert.equal(isProductionBillingHost("app.openworklabs.com"), true);
+ assert.equal(isProductionBillingHost("app.openworklabs.com:443"), true);
+ assert.equal(isProductionBillingHost("app.openworklabs.com, internal-proxy"), true);
+ assert.equal(isProductionBillingHost("localhost:3005"), false);
+ assert.equal(isProductionBillingHost(null), false);
+ });
+
+ test("derives displayed plan copy from price data", () => {
+ assert.deepEqual(
+ formatBillingPlanLabels({
+ amount: 5000,
+ currency: "usd",
+ recurringInterval: "month",
+ recurringIntervalCount: 1,
+ }),
+ {
+ amount: "$50.00",
+ cadence: "per month",
+ inline: "$50.00 per month",
+ available: true,
+ },
+ );
+ });
+
+ test("keeps non-monthly cadences tied to price data", () => {
+ assert.deepEqual(
+ formatBillingPlanLabels({
+ amount: 1200,
+ currency: "usd",
+ recurringInterval: "year",
+ recurringIntervalCount: 2,
+ }),
+ {
+ amount: "$12.00",
+ cadence: "every 2 years",
+ inline: "$12.00 every 2 years",
+ available: true,
+ },
+ );
+ });
+
+ test("uses clear copy when billing price is missing", () => {
+ assert.deepEqual(formatBillingPlanLabels(null), {
+ amount: "Price unavailable",
+ cadence: "billing cycle",
+ inline: "Price unavailable",
+ available: false,
+ });
+ });
+
+ test("does not invent amounts when billing amounts are missing", () => {
+ assert.equal(formatBillingAmountLabel(null, "usd"), "Not available");
+ assert.equal(formatBillingAmountLabel(undefined, "usd"), "Not available");
+ });
+
+ test("formats subscription status labels consistently", () => {
+ assert.equal(formatBillingStatusLabel("past_due"), "Past Due");
+ assert.equal(formatBillingStatusLabel("active"), "Active");
+ assert.equal(formatBillingStatusLabel(null), "Purchase required");
+ });
+
+ test("derives status from subscription before fallback plan state", () => {
+ assert.equal(
+ getBillingStatusLabel({
+ hasActivePlan: true,
+ subscription: {
+ id: "sub_1",
+ status: "trialing",
+ amount: 5000,
+ currency: "usd",
+ recurringInterval: "month",
+ recurringIntervalCount: 1,
+ currentPeriodStart: null,
+ currentPeriodEnd: null,
+ cancelAtPeriodEnd: false,
+ canceledAt: null,
+ endedAt: null,
+ },
+ }),
+ "Trialing",
+ );
+ assert.equal(getBillingStatusLabel({ hasActivePlan: true, subscription: null }), "Active");
+ assert.equal(getBillingStatusLabel({ hasActivePlan: false, subscription: null }), "Purchase required");
+ });
+
+ test("builds API-shaped mock summary for local checkout screenshots", () => {
+ const summary = buildLocalMockBillingSummary("https://checkout.example");
+
+ assert.equal(summary.checkoutUrl, "https://checkout.example");
+ assert.equal(summary.hasActivePlan, false);
+ assert.equal(summary.checkoutRequired, true);
+ assert.deepEqual(summary.invoices, []);
+ assert.deepEqual(summary.price, {
+ amount: 5000,
+ currency: "usd",
+ recurringInterval: "month",
+ recurringIntervalCount: 1,
+ });
+ });
+
+ test("keeps plan entitlement copy centralized", () => {
+ assert.equal(getWorkspacePlanEntitlementCopy(), "Includes up to 5 members and 1 hosted worker.");
+ assert.equal(getWorkspacePlanInlineEntitlementCopy(), "include up to 5 members and 1 hosted worker");
+ assert.equal(getWorkspacePlanShortEntitlementCopy(), "5 members included · 1 hosted worker");
+ });
+});
diff --git a/ee/apps/den-web/app/(den)/_lib/billing-display.ts b/ee/apps/den-web/app/(den)/_lib/billing-display.ts
new file mode 100644
index 000000000..3c47f8bdc
--- /dev/null
+++ b/ee/apps/den-web/app/(den)/_lib/billing-display.ts
@@ -0,0 +1,144 @@
+import {
+ formatMoneyMinor,
+ formatRecurringInterval,
+ type BillingPrice,
+ type BillingSummary,
+} from "./den-flow";
+
+export const WORKSPACE_PLAN_LIMITS = {
+ includedMembers: 5,
+ includedHostedWorkers: 1,
+} as const;
+
+export const LOCAL_MOCK_BILLING_PRICE: BillingPrice = {
+ amount: 5000,
+ currency: "usd",
+ recurringInterval: "month",
+ recurringIntervalCount: 1,
+};
+
+export type BillingPlanLabels = {
+ amount: string;
+ cadence: string;
+ inline: string;
+ available: boolean;
+};
+
+const PRODUCTION_BILLING_HOSTS = new Set(["app.openworklabs.com"]);
+
+function normalizeHost(value: string | null | undefined) {
+ const host = value?.split(",")[0]?.trim().toLowerCase();
+ if (!host) return null;
+ if (host.startsWith("[")) {
+ const end = host.indexOf("]");
+ return end > 0 ? host.slice(1, end) : host;
+ }
+ return host.split(":")[0] ?? null;
+}
+
+export function isProductionBillingHost(host: string | null | undefined) {
+ const normalizedHost = normalizeHost(host);
+ return normalizedHost ? PRODUCTION_BILLING_HOSTS.has(normalizedHost) : false;
+}
+
+export function isLocalMockBillingEnabled({
+ flag,
+ host,
+ nodeEnv,
+}: {
+ flag: string | undefined;
+ host?: string | null;
+ nodeEnv: string | undefined;
+}) {
+ return flag === "1" && nodeEnv !== "production" && !isProductionBillingHost(host);
+}
+
+export function buildLocalMockBillingSummary(checkoutUrl: string | null): BillingSummary {
+ return {
+ featureGateEnabled: true,
+ hasActivePlan: false,
+ checkoutRequired: true,
+ checkoutUrl,
+ portalUrl: null,
+ price: LOCAL_MOCK_BILLING_PRICE,
+ subscription: null,
+ invoices: [],
+ productId: null,
+ benefitId: null,
+ };
+}
+
+export function formatBillingPlanLabels(price: BillingPrice | null): BillingPlanLabels {
+ if (!price || price.amount === null) {
+ return {
+ amount: "Price unavailable",
+ cadence: "billing cycle",
+ inline: "Price unavailable",
+ available: false,
+ };
+ }
+
+ const amount = formatMoneyMinor(price.amount, price.currency);
+ const cadence = formatRecurringInterval(price.recurringInterval, price.recurringIntervalCount);
+
+ return {
+ amount,
+ cadence,
+ inline: `${amount} ${cadence}`,
+ available: true,
+ };
+}
+
+export function formatBillingAmountLabel(amount: number | null | undefined, currency: string | null | undefined) {
+ if (amount === null || amount === undefined) {
+ return "Not available";
+ }
+
+ return formatMoneyMinor(amount, currency ?? null);
+}
+
+export function formatBillingStatusLabel(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(" ");
+}
+
+export function getBillingStatusLabel(
+ summary: Pick
| null | undefined,
+) {
+ const subscription = summary?.subscription;
+
+ if (subscription?.status) {
+ return formatBillingStatusLabel(subscription.status);
+ }
+
+ return summary?.hasActivePlan ? "Active" : "Purchase required";
+}
+
+function formatIncludedNoun(count: number, singular: string, plural: string) {
+ return `${count} ${count === 1 ? singular : plural}`;
+}
+
+export function getWorkspacePlanEntitlementCopy() {
+ const members = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedMembers, "member", "members");
+ const workers = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedHostedWorkers, "hosted worker", "hosted workers");
+
+ return `Includes up to ${members} and ${workers}.`;
+}
+
+export function getWorkspacePlanInlineEntitlementCopy() {
+ const members = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedMembers, "member", "members");
+ const workers = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedHostedWorkers, "hosted worker", "hosted workers");
+
+ return `include up to ${members} and ${workers}`;
+}
+
+export function getWorkspacePlanShortEntitlementCopy() {
+ const members = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedMembers, "member", "members");
+ const workers = formatIncludedNoun(WORKSPACE_PLAN_LIMITS.includedHostedWorkers, "hosted worker", "hosted workers");
+
+ return `${members} included · ${workers}`;
+}
diff --git a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx
index f19e202cd..5d25e7d62 100644
--- a/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx
+++ b/ee/apps/den-web/app/(den)/_providers/den-flow-provider.tsx
@@ -860,13 +860,6 @@ export function DenFlowProvider({ children }: { children: ReactNode }) {
return;
}
- if (cancelAtPeriodEnd && typeof window !== "undefined") {
- const confirmed = window.confirm("Cancel subscription at period end? You can still use your current billing period.");
- if (!confirmed) {
- return;
- }
- }
-
setBillingSubscriptionBusy(true);
setBillingError(null);
diff --git a/ee/apps/den-web/app/(den)/checkout/page.tsx b/ee/apps/den-web/app/(den)/checkout/page.tsx
index 419ca0b5b..3275a8b0a 100644
--- a/ee/apps/den-web/app/(den)/checkout/page.tsx
+++ b/ee/apps/den-web/app/(den)/checkout/page.tsx
@@ -1,15 +1,22 @@
+import { headers } from "next/headers";
import { CheckoutScreen } from "../_components/checkout-screen";
+// Host is part of the local billing mock guard, so this page must stay per-request.
+export const dynamic = "force-dynamic";
+
export default async function CheckoutPage({
searchParams,
}: {
searchParams?: Promise<{ customer_session_token?: string }>;
}) {
const resolvedSearchParams = await searchParams;
+ const requestHeaders = await headers();
+ const requestHost = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host");
return (
);
}
diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/billing-dashboard-screen.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/billing-dashboard-screen.tsx
index 442a4a77f..4e99ab9e1 100644
--- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/billing-dashboard-screen.tsx
+++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/billing-dashboard-screen.tsx
@@ -1,17 +1,84 @@
"use client";
-import { useEffect } from "react";
-import { CreditCard } from "lucide-react";
+import { useEffect, useState } from "react";
+import { AlertTriangle, CreditCard, ExternalLink, RefreshCw } from "lucide-react";
import { DenButton, buttonVariants } from "../../../../_components/ui/button";
import {
- formatIsoDate,
- formatMoneyMinor,
- formatRecurringInterval,
- formatSubscriptionStatus,
-} from "../../../../_lib/den-flow";
+ formatBillingAmountLabel,
+ formatBillingPlanLabels,
+ getBillingStatusLabel,
+ getWorkspacePlanInlineEntitlementCopy,
+ getWorkspacePlanShortEntitlementCopy,
+} from "../../../../_lib/billing-display";
+import { formatIsoDate } from "../../../../_lib/den-flow";
import { DashboardPageTemplate } from "../../../../_components/ui/dashboard-page-template";
import { useDenFlow } from "../../../../_providers/den-flow-provider";
+function BillingMetric({ label, value }: { label: string; value: string | number }) {
+ return (
+
+ );
+}
+
+function CancelPlanDialog({
+ open,
+ effectiveDate,
+ busy,
+ onClose,
+ onConfirm,
+}: {
+ open: boolean;
+ effectiveDate: string | null;
+ busy: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+}) {
+ if (!open) {
+ return null;
+ }
+
+ return (
+
+
event.stopPropagation()}
+ >
+
+
+
+
+ Cancel plan?
+
+
+ You'll keep access until {effectiveDate ?? "the end of the current billing period"}.
+
+
+ You can resume the plan before then if you change your mind.
+
+
+
+
+
+
+ Keep plan
+
+
+ Cancel plan
+
+
+
+
+ );
+}
+
export function BillingDashboardScreen() {
const {
sessionHydrated,
@@ -25,6 +92,7 @@ export function BillingDashboardScreen() {
refreshBilling,
handleSubscriptionCancellation,
} = useDenFlow();
+ const [cancelPlanOpen, setCancelPlanOpen] = useState(false);
useEffect(() => {
if (!sessionHydrated || !user || billingSummary || billingBusy || billingCheckoutBusy) {
@@ -58,25 +126,22 @@ export function BillingDashboardScreen() {
const billingPrice = billingSummary?.price ?? null;
const subscription = billingSummary?.subscription ?? null;
- const planAmountLabel = billingPrice
- ? `${formatMoneyMinor(billingPrice.amount, billingPrice.currency)} · ${formatRecurringInterval(
- billingPrice.recurringInterval,
- billingPrice.recurringIntervalCount,
- )}`
- : "Not available";
- const statusLabel = subscription
- ? formatSubscriptionStatus(subscription.status)
- : billingSummary?.hasActivePlan
- ? "Active"
- : "Purchase required";
+ const planLabels = formatBillingPlanLabels(billingPrice);
+ const statusLabel = getBillingStatusLabel(billingSummary);
const nextBillingDate = subscription?.currentPeriodEnd
? formatIsoDate(subscription.currentPeriodEnd)
: "Not available";
- const nextPaymentAmount = subscription?.amount
- ? formatMoneyMinor(subscription.amount, subscription.currency)
- : billingPrice
- ? formatMoneyMinor(billingPrice.amount, billingPrice.currency)
- : "Not available";
+ const nextPaymentAmount = subscription
+ ? formatBillingAmountLabel(subscription.amount, subscription.currency)
+ : "Not available";
+ const workspacePlanDescription = billingSummary?.hasActivePlan
+ ? `This workspace's plan is ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.`
+ : planLabels.available
+ ? `Workspace plans are ${planLabels.inline} and ${getWorkspacePlanInlineEntitlementCopy()}.`
+ : "Workspace plan pricing is unavailable. Refresh billing to retry.";
+ const cancellationEffectiveDate = subscription?.currentPeriodEnd
+ ? formatIsoDate(subscription.currentPeriodEnd)
+ : null;
return (
) : null}
-
-
-
-
- {billingSummary?.hasActivePlan
- ? `This workspace's plan is currently ${statusLabel.toLowerCase()} and renews on ${nextBillingDate}.`
- : "Workspace plans are $50/month and include up to 5 members plus 1 hosted worker."}
-
+ {!billingSummary && !billingBusy && !billingCheckoutBusy ? (
+
+ Billing details are unavailable. Refresh billing to retry.
+ ) : null}
-
-
-
Current plan
-
{statusLabel}
-
-
-
-
Plan cost
-
{planAmountLabel}
-
-
-
-
Next billing date
-
{nextBillingDate}
-
+
+
+
+
+
+ Workspace plan
+
+
+ {workspacePlanDescription}
+
+
-
-
Next payment amount
-
{nextPaymentAmount}
+
+
+
+
+
+
-
-
Billing period
-
- {billingPrice
- ? formatRecurringInterval(
- billingPrice.recurringInterval,
- billingPrice.recurringIntervalCount,
- )
- : "Not available"}
-
-
-
-
-
Invoices
-
- {billingSummary?.invoices.length ?? 0}
-
-
+
-
+
{effectiveCheckoutUrl && !billingSummary?.hasActivePlan ? (
+
Purchase plan
) : null}
{billingSummary?.portalUrl ? (
+
Open billing portal
) : null}
@@ -158,7 +215,13 @@ export function BillingDashboardScreen() {
void handleSubscriptionCancellation(!Boolean(subscription?.cancelAtPeriodEnd))}
+ onClick={() => {
+ if (subscription?.cancelAtPeriodEnd) {
+ void handleSubscriptionCancellation(false);
+ return;
+ }
+ setCancelPlanOpen(true);
+ }}
>
{subscription?.cancelAtPeriodEnd ? "Resume plan" : "Cancel plan"}
@@ -166,20 +229,27 @@ export function BillingDashboardScreen() {
-
-
Pricing
+
+
+
-
+
Solo
-
$0
+
$0
Free forever · open source
-
+
Workspace plan
-
$50/month
-
5 members included · 1 hosted worker
+
+ {planLabels.amount} {planLabels.cadence}
+
+
{getWorkspacePlanShortEntitlementCopy()}
-
+
Enterprise
Custom
Windows included · talk to us
@@ -187,7 +257,7 @@ export function BillingDashboardScreen() {
-
+
Invoices
@@ -197,12 +267,14 @@ export function BillingDashboardScreen() {
{billingSummary?.portalUrl ? (
+
View invoices
) : (
void refreshBilling({ includeCheckout: true, quiet: false })}
>
@@ -210,6 +282,17 @@ export function BillingDashboardScreen() {
)}
+
{
+ if (!billingSubscriptionBusy) setCancelPlanOpen(false);
+ }}
+ onConfirm={() => {
+ void handleSubscriptionCancellation(true).then(() => setCancelPlanOpen(false));
+ }}
+ />
);
}
diff --git a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx
index b33004e18..2ef49c935 100644
--- a/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx
+++ b/ee/apps/den-web/app/(den)/o/[orgSlug]/dashboard/_components/org-dashboard-shell.tsx
@@ -249,7 +249,7 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
{switcherOpen ? (
-
+
{user?.email ?? "OpenWork user"}
@@ -281,7 +281,7 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
>
-
{org.name}
+
{org.name}
{org.role === "owner" ? "Creator plan" : "Free plan"} • 1 member
@@ -324,8 +324,8 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
);
return (
-
-
+
+
@@ -352,10 +352,10 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
@@ -363,7 +363,7 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
{item.label}
{item.badge ? (
-
+
{item.badge}
) : null}
@@ -389,7 +389,7 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
-
+
{pageTitle}
@@ -416,7 +416,7 @@ export function OrgDashboardShell({ children }: { children: React.ReactNode }) {
- {children}
+ {children}
);
diff --git a/ee/apps/den-web/app/globals.css b/ee/apps/den-web/app/globals.css
index 052f11e57..686820c3f 100644
--- a/ee/apps/den-web/app/globals.css
+++ b/ee/apps/den-web/app/globals.css
@@ -15,8 +15,8 @@
--dls-text-secondary: #667085;
--dls-hover: #f2f4f7;
--dls-active: #eef2f7;
- --dls-shell-shadow: 0 18px 40px -30px rgba(15, 23, 42, 0.14);
- --dls-card-shadow: 0 12px 24px -20px rgba(15, 23, 42, 0.1);
+ --dls-shell-shadow: 0 24px 48px -36px rgba(15, 23, 42, 0.2);
+ --dls-card-shadow: 0 16px 32px -28px rgba(15, 23, 42, 0.14);
--ow-bg: var(--dls-app-bg);
--ow-ink: var(--dls-text-primary);
--ow-muted: var(--dls-text-secondary);
@@ -42,10 +42,19 @@ body {
margin: 0;
font-family:
var(--font-inter), "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif;
+ font-feature-settings: "ss01";
+ font-optical-sizing: auto;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
color: var(--ow-ink);
background: var(--ow-bg);
}
+.den-tabular {
+ font-variant-numeric: tabular-nums;
+ font-feature-settings: "ss01", "tnum";
+}
+
.den-page {
width: min(100%, 1180px);
margin: 0 auto;
@@ -55,21 +64,21 @@ body {
.den-frame,
.den-frame-soft,
.den-frame-inset {
- border-radius: 2rem;
+ border-radius: 1.5rem;
border: 1px solid var(--dls-border);
}
.den-frame {
- background: rgba(255, 255, 255, 0.96);
+ background: rgba(255, 255, 255, 0.98);
box-shadow: var(--dls-shell-shadow);
}
.den-frame-soft {
- background: rgba(255, 255, 255, 0.92);
+ background: rgba(255, 255, 255, 0.96);
}
.den-frame-inset {
- background: rgba(248, 250, 252, 0.86);
+ background: #f8fafc;
}
.den-eyebrow {
@@ -82,18 +91,18 @@ body {
.den-title-xl {
margin: 0;
- font-size: clamp(2.1rem, 5vw, 4rem);
- line-height: 0.97;
- letter-spacing: -0.06em;
+ font-size: 3.35rem;
+ line-height: 1;
+ letter-spacing: 0;
font-weight: 600;
color: var(--dls-text-primary);
}
.den-title-lg {
margin: 0;
- font-size: clamp(1.6rem, 3vw, 2.4rem);
- line-height: 1;
- letter-spacing: -0.05em;
+ font-size: 2rem;
+ line-height: 1.08;
+ letter-spacing: 0;
font-weight: 600;
color: var(--dls-text-primary);
}
@@ -102,7 +111,17 @@ body {
margin: 0;
color: var(--dls-text-secondary);
font-size: 0.96rem;
- line-height: 1.75;
+ line-height: 1.65;
+}
+
+@media (max-width: 640px) {
+ .den-title-xl {
+ font-size: 2.35rem;
+ }
+
+ .den-title-lg {
+ font-size: 1.65rem;
+ }
}
.den-copy-strong {
@@ -122,6 +141,7 @@ body {
border-radius: 9999px;
font-size: 0.92rem;
font-weight: 600;
+ letter-spacing: 0;
transition:
color 0.2s ease,
background-color 0.2s ease,
@@ -133,7 +153,7 @@ body {
.den-button-primary {
background: #011627;
color: #fff;
- box-shadow: 0 14px 32px -20px rgba(1, 22, 39, 0.45);
+ box-shadow: 0 14px 26px -20px rgba(1, 22, 39, 0.5);
}
.den-button-primary:hover:not(:disabled) {
@@ -185,12 +205,13 @@ body {
.den-select,
.den-textarea {
width: 100%;
- border-radius: 1rem;
+ border-radius: 0.875rem;
border: 1px solid var(--dls-border);
background: #fff;
color: var(--dls-text-primary);
padding: 0.9rem 1rem;
font-size: 0.95rem;
+ letter-spacing: 0;
line-height: 1.4;
transition:
border-color 0.16s ease,
@@ -226,10 +247,11 @@ body {
}
.den-stat-card {
- border-radius: 1.5rem;
+ border-radius: 1.25rem;
border: 1px solid var(--dls-border);
- background: rgba(255, 255, 255, 0.92);
+ background: rgba(255, 255, 255, 0.96);
padding: 1.25rem;
+ box-shadow: var(--dls-card-shadow);
}
.den-stat-label {
@@ -245,7 +267,7 @@ body {
margin: 0.45rem 0 0;
font-size: 2rem;
line-height: 1;
- letter-spacing: -0.05em;
+ letter-spacing: 0;
font-weight: 600;
color: var(--dls-text-primary);
}
@@ -259,7 +281,7 @@ body {
.den-list-shell {
overflow: hidden;
- border-radius: 1.75rem;
+ border-radius: 1.25rem;
border: 1px solid var(--dls-border);
background: rgba(255, 255, 255, 0.96);
}
@@ -315,7 +337,7 @@ body {
}
.den-notice {
- border-radius: 1.25rem;
+ border-radius: 0.875rem;
padding: 0.9rem 1rem;
font-size: 0.92rem;
line-height: 1.6;
@@ -481,7 +503,7 @@ body {
.ow-brand-text {
font-size: 0.98rem;
font-weight: 600;
- letter-spacing: -0.02em;
+ letter-spacing: 0;
}
.ow-card {
@@ -574,9 +596,15 @@ body {
.ow-title {
margin: 0;
- font-size: clamp(1.7rem, 5vw, 2.1rem);
+ font-size: 2rem;
font-weight: 700;
- letter-spacing: -0.02em;
+ letter-spacing: 0;
+}
+
+@media (max-width: 640px) {
+ .ow-title {
+ font-size: 1.7rem;
+ }
}
.ow-subtitle {
@@ -728,41 +756,6 @@ body {
background: #d9e1f2;
}
-.ow-social-btn {
- width: 100%;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 0.68rem;
- padding: 0.78rem 0.92rem;
- font-size: 0.92rem;
-}
-
-.ow-social-icon {
- width: 1rem;
- height: 1rem;
- flex: 0 0 auto;
-}
-
-.ow-divider {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- color: #64748b;
- font-size: 0.78rem;
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.ow-divider::before,
-.ow-divider::after {
- content: "";
- flex: 1 1 auto;
- height: 1px;
- background: #d9e1f2;
-}
-
.ow-inline-row {
display: flex;
align-items: center;
diff --git a/ee/apps/den-web/components/den-marketing-rail.tsx b/ee/apps/den-web/components/den-marketing-rail.tsx
index 506ed1baa..3c4e89ed4 100644
--- a/ee/apps/den-web/components/den-marketing-rail.tsx
+++ b/ee/apps/den-web/components/den-marketing-rail.tsx
@@ -81,7 +81,7 @@ function PricingCards() {
Human repetitive work
- $2k-4k/mo
+ $2k-4k/mo
Best when the work needs constant human judgment.
Expensive for follow-through and reminders.
@@ -95,7 +95,7 @@ function PricingCards() {
Recommended
-
$50/mo
+
Cloud plan
Handles repetitive work continuously instead of in bursts.
Keeps humans focused on approvals and exceptions.
@@ -113,7 +113,7 @@ export function DenMarketingRail({ compact = false }: DenMarketingRailProps) {
OpenWork hosted
-
+
Always-on AI workers for you and your team.
@@ -132,7 +132,7 @@ export function DenMarketingRail({ compact = false }: DenMarketingRailProps) {
{card.label}
- {card.title}
+ {card.title}
{card.body}
{card.detail}
@@ -146,8 +146,8 @@ export function DenMarketingRail({ compact = false }: DenMarketingRailProps) {
Pricing
-
- Replace repetitive work with a $50 worker.
+
+ Replace repetitive work with a cloud worker.
Start free, then use Den Cloud billing when you need more capacity. The app handles checkout return flows and billing management without sending people back to marketing pages.
diff --git a/ee/apps/den-web/docs/screenshots/billing-dashboard-after-desktop.png b/ee/apps/den-web/docs/screenshots/billing-dashboard-after-desktop.png
new file mode 100644
index 000000000..9cccc1bfa
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/billing-dashboard-after-desktop.png differ
diff --git a/ee/apps/den-web/docs/screenshots/billing-dashboard-before-desktop.png b/ee/apps/den-web/docs/screenshots/billing-dashboard-before-desktop.png
new file mode 100644
index 000000000..ff477cdfa
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/billing-dashboard-before-desktop.png differ
diff --git a/ee/apps/den-web/docs/screenshots/checkout-billing-after-desktop.png b/ee/apps/den-web/docs/screenshots/checkout-billing-after-desktop.png
new file mode 100644
index 000000000..cec38ea65
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/checkout-billing-after-desktop.png differ
diff --git a/ee/apps/den-web/docs/screenshots/checkout-billing-after-mobile.png b/ee/apps/den-web/docs/screenshots/checkout-billing-after-mobile.png
new file mode 100644
index 000000000..10ba76c6b
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/checkout-billing-after-mobile.png differ
diff --git a/ee/apps/den-web/docs/screenshots/checkout-billing-before-desktop.png b/ee/apps/den-web/docs/screenshots/checkout-billing-before-desktop.png
new file mode 100644
index 000000000..e9fdcb02a
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/checkout-billing-before-desktop.png differ
diff --git a/ee/apps/den-web/docs/screenshots/checkout-billing-before-mobile.png b/ee/apps/den-web/docs/screenshots/checkout-billing-before-mobile.png
new file mode 100644
index 000000000..ac74d1f8d
Binary files /dev/null and b/ee/apps/den-web/docs/screenshots/checkout-billing-before-mobile.png differ
diff --git a/ee/apps/den-web/package.json b/ee/apps/den-web/package.json
index 3a06afb66..264526544 100644
--- a/ee/apps/den-web/package.json
+++ b/ee/apps/den-web/package.json
@@ -8,7 +8,8 @@
"prebuild": "pnpm --dir ../../../packages/ui build && pnpm --dir ../../packages/utils build",
"build": "next build",
"start": "next start --hostname 0.0.0.0 --port 3005",
- "lint": "next lint"
+ "lint": "next lint",
+ "test:billing": "tsx --test 'app/(den)/_lib/billing-display.test.ts'"
},
"dependencies": {
"@openwork/types": "workspace:*",
@@ -28,6 +29,7 @@
"autoprefixer": "10.4.19",
"postcss": "8.4.38",
"tailwindcss": "3.4.7",
+ "tsx": "^4.21.0",
"typescript": "5.4.5"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6cdcf3fd5..23b5d64bc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -567,6 +567,9 @@ importers:
tailwindcss:
specifier: 3.4.7
version: 3.4.7
+ tsx:
+ specifier: ^4.21.0
+ version: 4.21.0
typescript:
specifier: 5.4.5
version: 5.4.5