Skip to content

Commit 5ea65bf

Browse files
committed
feat(website): redesign pricing and licensing journey
1 parent 6700e61 commit 5ea65bf

34 files changed

Lines changed: 1403 additions & 566 deletions

apps/minting-service/src/lib/email.spec.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
2-
import { renderLicenseEmail } from './email.js';
2+
import { renderLicenseEmail, renderRevocationEmail } from './email.js';
33

44
describe('renderLicenseEmail', () => {
55
it('includes the token wrapped in BEGIN/END delimiters in the text body', () => {
@@ -16,15 +16,19 @@ describe('renderLicenseEmail', () => {
1616
expect(out.text).toContain('-----END THREADPLANE LICENSE-----');
1717
});
1818

19-
it('subject includes tier and seat count with plural s for seats > 1', () => {
19+
it('uses the public Pro plan name and plural seats in customer-facing copy', () => {
2020
const out = renderLicenseEmail({
2121
tier: 'developer_seat',
2222
seats: 3,
2323
token: 't.s',
2424
expiresAt: new Date('2027-04-20T00:00:00Z'),
2525
stripeCustomerId: 'cus_test',
2626
});
27-
expect(out.subject).toBe('Your Threadplane license — developer_seat (3 seats)');
27+
expect(out.subject).toBe('Your Threadplane Pro license — 3 seats');
28+
expect(out.text).toContain('Plan: Pro');
29+
expect(out.html).toContain('<strong>Plan:</strong> Pro');
30+
expect(out.subject).not.toContain('developer_seat');
31+
expect(out.text).not.toContain('developer_seat');
2832
});
2933

3034
it('subject uses singular seat for seats === 1', () => {
@@ -35,7 +39,7 @@ describe('renderLicenseEmail', () => {
3539
expiresAt: new Date('2027-04-20T00:00:00Z'),
3640
stripeCustomerId: 'cus_test',
3741
});
38-
expect(out.subject).toBe('Your Threadplane license — team (1 seat)');
42+
expect(out.subject).toBe('Your Threadplane Team license — 1 seat');
3943
});
4044

4145
it('includes ISO 8601 UTC expiry in text body', () => {
@@ -62,3 +66,15 @@ describe('renderLicenseEmail', () => {
6266
expect(out.html).toContain('BEGIN THREADPLANE LICENSE');
6367
});
6468
});
69+
70+
describe('renderRevocationEmail', () => {
71+
it('describes the record change without claiming offline signature checks perform revocation lookup', () => {
72+
const out = renderRevocationEmail({ tier: 'developer_seat' });
73+
74+
expect(out.text).toContain('Threadplane Pro license');
75+
expect(out.text).toContain('marked revoked in Threadplane records');
76+
expect(out.text).toContain('Runtime verification remains offline');
77+
expect(out.text).not.toContain('fail signature checks');
78+
expect(out.html).not.toContain('developer_seat');
79+
});
80+
});

apps/minting-service/src/lib/email.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ export interface RenderedEmail {
2828
html: string;
2929
}
3030

31+
function publicTierName(tier: MintableTier): string {
32+
return tier === 'developer_seat' ? 'Pro' : 'Team';
33+
}
34+
3135
/**
3236
* Pure: render the subject / text / html for a license delivery email.
3337
*/
3438
export function renderLicenseEmail(vars: LicenseEmailVars): RenderedEmail {
3539
const seatWord = vars.seats === 1 ? 'seat' : 'seats';
36-
const subject = `Your Threadplane license — ${vars.tier} (${vars.seats} ${seatWord})`;
40+
const planName = publicTierName(vars.tier);
41+
const subject = `Your Threadplane ${planName} license — ${vars.seats} ${seatWord}`;
3742
const expiresIso = vars.expiresAt.toISOString();
3843

3944
const portal = portalUrl(vars.stripeCustomerId);
@@ -47,7 +52,7 @@ the link at the bottom of this email.
4752
${vars.token}
4853
-----END THREADPLANE LICENSE-----
4954
50-
Tier: ${vars.tier}
55+
Plan: ${planName}
5156
Seats: ${vars.seats}
5257
Expires: ${expiresIso}
5358
@@ -72,7 +77,7 @@ Questions: reply to this email.
7277
<pre style="white-space:pre-wrap;word-break:break-all;font-family:monospace;font-size:12px;background:#f4f4f4;padding:12px;border-radius:4px">-----BEGIN THREADPLANE LICENSE-----
7378
${escapeHtml(vars.token)}
7479
-----END THREADPLANE LICENSE-----</pre>
75-
<p><strong>Tier:</strong> ${escapeHtml(vars.tier)}<br>
80+
<p><strong>Plan:</strong> ${escapeHtml(planName)}<br>
7681
<strong>Seats:</strong> ${vars.seats}<br>
7782
<strong>Expires:</strong> ${escapeHtml(expiresIso)}</p>
7883
<p><strong>Installation:</strong></p>
@@ -131,20 +136,20 @@ export interface RevocationEmailVars {
131136

132137
export function renderRevocationEmail(vars: RevocationEmailVars): RenderedEmail {
133138
const subject = `Your Threadplane license has been revoked`;
139+
const planName = publicTierName(vars.tier);
134140

135-
const text = `Your Threadplane ${vars.tier} license has been revoked because the
136-
underlying payment was refunded.
141+
const text = `Your Threadplane ${planName} license has been marked revoked in Threadplane records because the underlying payment was refunded.
137142
138-
The token previously delivered will fail signature checks at boot and
139-
@threadplane/chat will fall back to a noncommercial-use warning.
143+
Runtime verification remains offline and does not make a revocation lookup.
144+
Continued use must remain within the applicable license terms.
140145
141146
If you believe this is in error, reply to this email.
142147
143148
-- The Threadplane team
144149
`;
145150

146-
const html = `<p>Your Threadplane <strong>${escapeHtml(vars.tier)}</strong> license has been revoked because the underlying payment was refunded.</p>
147-
<p>The token previously delivered will fail signature checks at boot and <code>@threadplane/chat</code> will fall back to a noncommercial-use warning.</p>
151+
const html = `<p>Your Threadplane <strong>${escapeHtml(planName)}</strong> license has been marked revoked in Threadplane records because the underlying payment was refunded.</p>
152+
<p>Runtime verification remains offline and does not make a revocation lookup. Continued use must remain within the applicable license terms.</p>
148153
<p>If you believe this is in error, reply to this email.</p>
149154
<p>-- The Threadplane team</p>
150155
`;

apps/website/content/AGENTS.md.template

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Threadplane v@VERSION@
22

3-
Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
3+
Production-ready chat, thread/history/branch UI, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
4+
5+
## Licensing and deployment boundary
6+
- Most Threadplane packages are MIT-licensed and free for any use.
7+
- `@threadplane/chat` is source-available under PolyForm Noncommercial 1.0.0, includes a good-faith 30-day commercial evaluation, and requires a Threadplane Commercial license for commercial production.
8+
- Threadplane runs inside the customer's Angular application. Agent runtime, models, storage, checkpointing, retention, authorization, and hosting remain customer-operated.
9+
- License-token verification is offline and advisory; it makes no runtime call to Threadplane and never blocks rendering.
410

511
## Install
612
npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked
@@ -34,7 +40,7 @@ export class ChatComponent {
3440
```
3541

3642
## Key patterns
37-
- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`
43+
- Thread selection: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`; actual durability and cross-device persistence depend on the connected runtime and persistence layer
3844
- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts
3945
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
4046
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself

apps/website/content/CLAUDE.md.template

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Threadplane v@VERSION@
22

3-
Production-ready chat, durable threads, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
3+
Production-ready chat, thread/history/branch UI, interrupts, subagents, planning, memory, and generative UI for Angular agent apps.
4+
5+
## Licensing and deployment boundary
6+
- Most Threadplane packages are MIT-licensed and free for any use.
7+
- `@threadplane/chat` is source-available under PolyForm Noncommercial 1.0.0, includes a good-faith 30-day commercial evaluation, and requires a Threadplane Commercial license for commercial production.
8+
- Threadplane runs inside the customer's Angular application. Agent runtime, models, storage, checkpointing, retention, authorization, and hosting remain customer-operated.
9+
- License-token verification is offline and advisory; it makes no runtime call to Threadplane and never blocks rendering.
410

511
## Install
612
npm install @threadplane/chat @threadplane/langgraph @langchain/core @langchain/langgraph-sdk marked
@@ -34,7 +40,7 @@ export class ChatComponent {
3440
```
3541

3642
## Key patterns
37-
- Thread persistence: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`
43+
- Thread selection: configure `provideAgent({ assistantId, threadId: signal(localStorage.getItem('t')), onThreadId })`; actual durability and cross-device persistence depend on the connected runtime and persistence layer
3844
- Global config: `provideAgent({ apiUrl, assistantId })` in app.config.ts
3945
- Scoped config: re-provide `provideAgent({ apiUrl, assistantId })` in a component `providers` array for a subtree
4046
- Testing: use `MockAgentTransport` — never mock `injectAgent()` itself

apps/website/content/docs/chat/getting-started/installation.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A complete walkthrough for installing `@threadplane/chat` in an Angular 20+ appl
55
This guide goes deeper than the Quick Start: license activation, peer dependencies, and the warnings you'll see in the console.
66

77
<Callout type="info" title="Just bought a license?">
8-
This guide is written for customers who purchased a Developer Seat, Team, or Enterprise plan and received a `THREADPLANE_LICENSE` token by email. If you're evaluating `@threadplane/chat` for noncommercial use, you can skip the license steps — the library runs without a token (with a one-time advisory warning).
8+
This guide is written for customers who purchased a Pro, Team, or Enterprise plan and received a `THREADPLANE_LICENSE` token by email. If you're using `@threadplane/chat` within permitted noncommercial scope, you can skip the license steps. Commercial evaluation is permitted for 30 calendar days from first commercial use without registration; the library's token check remains advisory.
99
</Callout>
1010

1111
## Prerequisites
@@ -21,7 +21,7 @@ Required for the Angular build toolchain. `node --version` should report `v18` o
2121
The chat UI needs something to talk to. Two officially supported adapters cover virtually every backend:
2222

2323
- **`@threadplane/langgraph`** — pick this if your backend is LangGraph or LangGraph Platform.
24-
- **`@threadplane/ag-ui`** — pick this for any AG-UI compatible backend (CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, AWS Strands, CopilotKit runtime).
24+
- **`@threadplane/ag-ui`** — pick this for an AG-UI-compatible backend such as CrewAI, Mastra, Microsoft Agent Framework, AG2, Pydantic AI, or AWS Strands.
2525

2626
Both adapters expose the same `Agent` contract to `@threadplane/chat`, so swapping later is a one-line change. If you don't have a backend yet, use `mockAgent()` from `@threadplane/chat` to wire up the UI first.
2727
</Step>

apps/website/e2e/website.spec.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,35 @@ test('landing page license copy distinguishes MIT packages from commercially lic
3232
await expect(main).not.toContainText('MIT · No signup required · App telemetry off by default');
3333
});
3434

35-
test('pricing page shows plan cards', async ({ page }) => {
35+
test('pricing page presents the four-stage journey and licensing boundary', async ({ page }) => {
3636
await page.goto('/pricing');
37-
await expect(page.getByText('Community').first()).toBeVisible();
38-
await expect(page.getByText('Developer Seat').first()).toBeVisible();
39-
await expect(page.getByText('Team').first()).toBeVisible();
40-
await expect(page.getByText('Enterprise').first()).toBeVisible();
37+
const plans = page.locator('.pricing-plan-card');
38+
await expect(plans).toHaveCount(4);
39+
await expect(plans.nth(0).getByRole('heading', { level: 3 })).toHaveText('Developer');
40+
await expect(plans.nth(1).getByRole('heading', { level: 3 })).toHaveText('Pro');
41+
await expect(plans.nth(2).getByRole('heading', { level: 3 })).toHaveText('Team');
42+
await expect(plans.nth(3).getByRole('heading', { level: 3 })).toHaveText('Enterprise');
43+
await expect(plans.nth(0)).toContainText('For permitted noncommercial use');
44+
await expect(plans.nth(0)).toContainText('30-day commercial evaluation');
45+
await expect(page.getByText('No Threadplane cloud').first()).toBeVisible();
46+
});
47+
48+
test('pricing page preserves public-to-internal checkout mappings', async ({ page }) => {
49+
await page.goto('/pricing');
50+
51+
await expect(page.getByRole('button', { name: 'Get Pro' }).locator('xpath=ancestor::form/input[@name="tier"]')).toHaveValue('developer_seat');
52+
await expect(page.getByRole('button', { name: 'Get Team' }).locator('xpath=ancestor::form/input[@name="tier"]')).toHaveValue('team');
53+
await expect(page.getByRole('link', { name: 'Start free' })).toHaveAttribute('href', /npmjs\.com\/package\/@threadplane\/chat/);
54+
await expect(page.getByRole('link', { name: 'Talk to Sales' })).toHaveAttribute('href', '/contact?source=pricing_tier_enterprise');
55+
});
56+
57+
test('pricing page is responsive without page-level horizontal overflow', async ({ page }) => {
58+
for (const width of [375, 768, 1280]) {
59+
await page.setViewportSize({ width, height: 900 });
60+
await page.goto('/pricing');
61+
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
62+
expect(overflow, `pricing at ${width}px`).toBeLessThanOrEqual(1);
63+
}
4164
});
4265

4366
test('pricing page lead form validates required fields', async ({ page }) => {

apps/website/emails/drip-angular-followup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ export function dripAngularFollowupHtml(day: number): { subject: string; html: s
3333

3434
if (day === 10) {
3535
return {
36-
subject: 'The pilot program includes hands-on integration',
36+
subject: 'An optional eight-week path from pilot to production',
3737
html: wrapEmail({
3838
body: `
3939
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#004090;font-weight:700;margin:0 0 8px">Pilot Program</p>
40-
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
41-
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
40+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
41+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
4242
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(0,64,144,0.08)">
4343
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Week 1</strong> · Integration &amp; first stream</p>
4444
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#004090">Month 1</strong> · First agent in staging</p>
45-
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#004090">Month 3</strong> · Production deployment</p>
45+
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#004090">Week 8</strong> · Production readiness</p>
4646
</div>
4747
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#004090;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
4848
`,

apps/website/emails/drip-chat-followup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ export function dripChatFollowupHtml(day: number): { subject: string; html: stri
3333

3434
if (day === 10) {
3535
return {
36-
subject: 'The pilot program includes hands-on integration',
36+
subject: 'An optional eight-week path from pilot to production',
3737
html: wrapEmail({
3838
body: `
3939
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#5a00c8;font-weight:700;margin:0 0 8px">Pilot Program</p>
40-
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
41-
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
40+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
41+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
4242
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(90,0,200,0.08)">
4343
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#5a00c8">Week 1</strong> · Integration &amp; first stream</p>
4444
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#5a00c8">Month 1</strong> · First agent in staging</p>
45-
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#5a00c8">Month 3</strong> · Production deployment</p>
45+
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#5a00c8">Week 8</strong> · Production readiness</p>
4646
</div>
4747
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#5a00c8;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
4848
`,

apps/website/emails/drip-render-followup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ export function dripRenderFollowupHtml(day: number): { subject: string; html: st
3333

3434
if (day === 10) {
3535
return {
36-
subject: 'The pilot program includes hands-on integration',
36+
subject: 'An optional eight-week path from pilot to production',
3737
html: wrapEmail({
3838
body: `
3939
<p style="font-size:11px;font-family:monospace;text-transform:uppercase;letter-spacing:0.08em;color:#1a7a40;font-weight:700;margin:0 0 8px">Pilot Program</p>
40-
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">The pilot program includes hands-on integration</p>
41-
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Every app deployment license includes a <strong>3-month co-pilot engagement</strong> — we work alongside your Angular team to ship your first agent to production.</p>
40+
<p style="font-size:20px;font-weight:700;color:#1a1a2e;margin:0 0 14px;line-height:1.3">An optional eight-week path from pilot to production</p>
41+
<p style="font-size:14px;color:#555770;line-height:1.7;margin:0 0 14px">Pilot-to-Prod is a separately scoped <strong>eight-week engineering engagement</strong> for teams that want hands-on help shipping their first agent to production.</p>
4242
<div style="background:#f8f9fc;border-radius:8px;padding:16px 18px;margin:0 0 24px;border:1px solid rgba(26,122,64,0.08)">
4343
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#1a7a40">Week 1</strong> · Integration &amp; first stream</p>
4444
<p style="font-size:13px;color:#555770;margin:0 0 4px;line-height:1.6"><strong style="color:#1a7a40">Month 1</strong> · First agent in staging</p>
45-
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#1a7a40">Month 3</strong> · Production deployment</p>
45+
<p style="font-size:13px;color:#555770;margin:0;line-height:1.6"><strong style="color:#1a7a40">Week 8</strong> · Production readiness</p>
4646
</div>
4747
<a href="https://threadplane.ai/pilot-to-prod" style="display:inline-block;background-color:#1a7a40;color:#fff;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;text-decoration:none">Learn About the Pilot →</a>
4848
`,

0 commit comments

Comments
 (0)