@taskforcehq/taskforce 0.3.307 → 0.3.308

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  2. package/dist/components/views/PlanComparisonPage.js +214 -83
  3. package/dist/components/views/PlanComparisonPage.test.js +249 -14
  4. package/dist/components/views/PlansPage.js +15 -9
  5. package/dist/components/views/PlansPage.test.js +4 -2
  6. package/dist/core/GlobalSettingsService.js +18 -0
  7. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  8. package/dist/core/GlobalSettingsService.test.js +45 -0
  9. package/dist/core/PlanEntitlementService.d.ts +20 -0
  10. package/dist/core/PlanEntitlementService.js +215 -19
  11. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  12. package/dist/core/Taskforce.d.ts +14 -0
  13. package/dist/core/Taskforce.js +6 -0
  14. package/dist/core/types.d.ts +3 -0
  15. package/dist/migrations/taskSchemaMigrations.js +37 -15
  16. package/dist/server/routes/admin.js +146 -5
  17. package/dist/server/routes/authSupport.d.ts +1 -0
  18. package/dist/server/routes/authSupport.js +2 -1
  19. package/dist/server/routes/billing.d.ts +1 -0
  20. package/dist/server/routes/billing.js +105 -63
  21. package/dist/server/routes/billing.test.js +159 -11
  22. package/dist/server/routes.test.js +91 -10
  23. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  24. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  25. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  26. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  27. package/dist/ui/agent-logos/codex.jpeg +0 -0
  28. package/dist/ui/agent-logos/cursor.png +0 -0
  29. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  30. package/dist/ui/agent-logos/windsurf.png +0 -0
  31. package/dist/ui/assets/{AgentsModule-2y82_3DO.js → AgentsModule-BS4Pi_nC.js} +1 -1
  32. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTLZZCiQ.js → AnnotatedAttachmentWorkspace-CCckzWYZ.js} +1 -1
  33. package/dist/ui/assets/{ContextAttachmentManager-D2epuNnG.js → ContextAttachmentManager-CfG_ER7C.js} +1 -1
  34. package/dist/ui/assets/{DocumentWorkspace-sVa-3Do6.js → DocumentWorkspace-BVaWPA25.js} +1 -1
  35. package/dist/ui/assets/{EntityActivityTimeline-Zods27y_.js → EntityActivityTimeline-DSj0ThaK.js} +1 -1
  36. package/dist/ui/assets/{InitiativesModule-D4XseTwW.js → InitiativesModule-AxMkWtxH.js} +1 -1
  37. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  38. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  39. package/dist/ui/assets/{TaskContextUpload-uC5qx4Bv.js → TaskContextUpload-Dw4rTF4i.js} +1 -1
  40. package/dist/ui/assets/{TaskSettings-CqvPAsTv.js → TaskSettings-f2ga42_V.js} +1 -1
  41. package/dist/ui/assets/{WorkflowsModule-oOA9bcdK.js → WorkflowsModule-E4UL3mov.js} +1 -1
  42. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  43. package/dist/ui/assets/{index-CYyodXsg.css → index-BvAbqx5Q.css} +1 -1
  44. package/dist/ui/assets/{index-607WPo_X.js → index-MAHKvFqp.js} +4 -4
  45. package/dist/ui/index.html +2 -2
  46. package/dist/ui/og-image.png +0 -0
  47. package/package.json +1 -1
  48. package/dist/ui/assets/PlansPage-D5AcbO0L.js +0 -1
  49. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +0 -1
  50. package/dist/ui/assets/documentReferences-DPZuTgHh.js +0 -1
@@ -1,6 +1,15 @@
1
1
  import React from 'react';
2
+ import type { TaskforceTheme } from '../../utils/theme';
2
3
  export type BillingInterval = 'month' | 'year';
3
4
  export type BillingPricingType = 'stripe' | 'free';
5
+ export type BillingPricingAudience = 'public' | 'campaign';
6
+ type PublicPricingValue = {
7
+ pricingType: BillingPricingType;
8
+ stripePriceId: string | null;
9
+ unitAmount: number | null;
10
+ currency: string | null;
11
+ interval: string;
12
+ } | null;
4
13
  export interface PublicPricingPlan {
5
14
  planId: string;
6
15
  displayName: string;
@@ -20,22 +29,15 @@ export interface PublicPricingPlan {
20
29
  description?: string | null;
21
30
  publicLabel?: string | null;
22
31
  publicDescription?: string | null;
32
+ publicDescriptionVisible?: boolean | null;
23
33
  }>;
24
34
  pricing: {
25
- month: {
26
- pricingType: BillingPricingType;
27
- stripePriceId: string | null;
28
- unitAmount: number | null;
29
- currency: string | null;
30
- interval: string;
31
- } | null;
32
- year: {
33
- pricingType: BillingPricingType;
34
- stripePriceId: string | null;
35
- unitAmount: number | null;
36
- currency: string | null;
37
- interval: string;
38
- } | null;
35
+ month: PublicPricingValue;
36
+ year: PublicPricingValue;
37
+ };
38
+ campaignPricing?: {
39
+ month: PublicPricingValue;
40
+ year: PublicPricingValue;
39
41
  };
40
42
  }
41
43
  interface PricingSourceDetails {
@@ -47,12 +49,14 @@ export interface PlanSelectionIntent {
47
49
  planVersionId: string;
48
50
  interval: BillingInterval;
49
51
  pricingType: BillingPricingType;
52
+ pricingAudience: BillingPricingAudience;
50
53
  isCurrentPlan: boolean;
51
54
  }
52
55
  interface PlanComparisonPageProps {
53
56
  heading: string;
54
57
  subtitle?: string;
55
58
  variant?: 'app' | 'site';
59
+ theme?: TaskforceTheme;
56
60
  chrome?: React.ReactNode;
57
61
  footer?: React.ReactNode;
58
62
  isAuthenticated: boolean;
@@ -74,9 +78,10 @@ interface PlanComparisonPageProps {
74
78
  shellOwnsScroll?: boolean;
75
79
  showPageBackButton?: boolean;
76
80
  showHeaderBarWithChrome?: boolean;
81
+ preferPricingPageTitle?: boolean;
77
82
  currentPlanCardActionMode?: 'manage' | 'indicator' | 'hidden';
78
83
  fallbackPricingSource?: PricingSourceDetails | null;
79
84
  onSelectPlan: (intent: PlanSelectionIntent) => void | Promise<void>;
80
85
  }
81
- export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, showHeaderBarWithChrome, currentPlanCardActionMode, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
86
+ export declare function PlanComparisonPage({ heading, subtitle, variant, theme, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, showHeaderBarWithChrome, preferPricingPageTitle, currentPlanCardActionMode, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
82
87
  export {};
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from 'react';
3
3
  import { CheckCircle2, Loader2 } from 'lucide-react';
4
4
  import taskforceStyles from '../../Taskforce.module.css';
@@ -19,6 +19,60 @@ function formatPricingSource(pricingSource) {
19
19
  return `${environment} (${runtimeMode})`;
20
20
  return environment || runtimeMode || null;
21
21
  }
22
+ const ALLOWED_PRICING_DESCRIPTION_TAGS = new Set(['a', 'b', 'br', 'em', 'i', 'li', 'ol', 'p', 'strong', 'u', 'ul']);
23
+ function escapeHtmlText(raw) {
24
+ return raw
25
+ .replace(/&/g, '&amp;')
26
+ .replace(/</g, '&lt;')
27
+ .replace(/>/g, '&gt;')
28
+ .replace(/"/g, '&quot;')
29
+ .replace(/'/g, '&#39;');
30
+ }
31
+ function sanitizePricingDescriptionHtml(raw) {
32
+ const value = String(raw || '').trim();
33
+ if (!value)
34
+ return '';
35
+ if (typeof document === 'undefined')
36
+ return escapeHtmlText(value);
37
+ const template = document.createElement('template');
38
+ template.innerHTML = value;
39
+ const output = document.createElement('div');
40
+ const cleanNode = (node) => {
41
+ if (node.nodeType === Node.TEXT_NODE) {
42
+ return document.createTextNode(node.textContent || '');
43
+ }
44
+ if (node.nodeType !== Node.ELEMENT_NODE)
45
+ return null;
46
+ const element = node;
47
+ const tagName = element.tagName.toLowerCase();
48
+ if (tagName === 'script' || tagName === 'style')
49
+ return null;
50
+ const children = Array.from(element.childNodes).map(cleanNode).filter((child) => Boolean(child));
51
+ if (!ALLOWED_PRICING_DESCRIPTION_TAGS.has(tagName)) {
52
+ const fragment = document.createDocumentFragment();
53
+ for (const child of children)
54
+ fragment.appendChild(child);
55
+ return fragment;
56
+ }
57
+ const next = document.createElement(tagName);
58
+ if (tagName === 'a') {
59
+ const href = String(element.getAttribute('href') || '').trim();
60
+ if (/^(https?:|mailto:|\/|#)/i.test(href)) {
61
+ next.setAttribute('href', href);
62
+ next.setAttribute('rel', 'noopener noreferrer');
63
+ }
64
+ }
65
+ for (const child of children)
66
+ next.appendChild(child);
67
+ return next;
68
+ };
69
+ for (const child of Array.from(template.content.childNodes)) {
70
+ const cleaned = cleanNode(child);
71
+ if (cleaned)
72
+ output.appendChild(cleaned);
73
+ }
74
+ return output.innerHTML;
75
+ }
22
76
  function formatCurrency(amount, currency) {
23
77
  if (amount == null || !currency)
24
78
  return 'Contact Sales';
@@ -34,7 +88,7 @@ function formatCurrency(amount, currency) {
34
88
  }
35
89
  }
36
90
  function renderFreePriceAmount() {
37
- return '$0.00';
91
+ return '$0';
38
92
  }
39
93
  function isSelectablePrice(price) {
40
94
  if (!price)
@@ -50,6 +104,25 @@ function renderPriceLabel(price) {
50
104
  return renderFreePriceAmount();
51
105
  return formatCurrency(price.unitAmount, price.currency);
52
106
  }
107
+ function getSelectedPricing(regularPrice, campaignPrice) {
108
+ return isSelectablePrice(campaignPrice)
109
+ ? { price: campaignPrice, pricingAudience: 'campaign' }
110
+ : { price: regularPrice, pricingAudience: 'public' };
111
+ }
112
+ function renderPriceRows(monthPrice, yearPrice, campaignMonthPrice, campaignYearPrice) {
113
+ const hasMonth = isSelectablePrice(monthPrice) || isSelectablePrice(campaignMonthPrice);
114
+ const hasYear = isSelectablePrice(yearPrice) || isSelectablePrice(campaignYearPrice);
115
+ if (!hasMonth && !hasYear)
116
+ return null;
117
+ const renderRow = (price, campaignPrice, interval) => {
118
+ if (!isSelectablePrice(price) && !isSelectablePrice(campaignPrice))
119
+ return null;
120
+ const hasCampaignPrice = isSelectablePrice(campaignPrice);
121
+ const intervalLabel = interval === 'year' ? 'year' : 'month';
122
+ return (_jsxs("div", { className: `${styles.priceRow} ${hasCampaignPrice ? styles.priceRowWithCampaign : ''}`, children: [isSelectablePrice(price) ? (_jsxs("div", { className: styles.regularPriceLine, children: [_jsx("span", { className: `${styles.priceRowAmount} ${hasCampaignPrice ? styles.regularPriceDiscounted : ''}`, children: renderPriceLabel(price) }), _jsxs("span", { className: styles.priceRowInterval, children: ["/ ", intervalLabel] })] })) : null, hasCampaignPrice ? (_jsxs("div", { className: styles.campaignPriceLine, children: [_jsx("span", { className: styles.campaignPriceLabel, children: "Founding Offer:" }), _jsx("span", { className: styles.campaignPriceAmount, children: renderPriceLabel(campaignPrice) }), _jsxs("span", { className: styles.campaignPriceInterval, children: ["/ ", intervalLabel] })] })) : null] }, interval));
123
+ };
124
+ return (_jsxs("div", { className: styles.priceRows, children: [renderRow(monthPrice, campaignMonthPrice, 'month'), renderRow(yearPrice, campaignYearPrice, 'year')] }));
125
+ }
53
126
  function getPlanSortRank(plan) {
54
127
  const prices = [plan.pricing.month, plan.pricing.year].filter(Boolean);
55
128
  if (prices.some((price) => price.pricingType === 'free'))
@@ -74,36 +147,69 @@ function readConfiguredFeatureLimit(config, key) {
74
147
  const normalized = Math.max(1, Math.floor(numericValue));
75
148
  return normalized;
76
149
  }
150
+ function formatLimitedFeatureLabel(baseLabel, limitValue) {
151
+ if (limitValue !== 1)
152
+ return baseLabel;
153
+ return baseLabel
154
+ .replace(/\b([A-Za-z]+)ies\b$/, '$1y')
155
+ .replace(/\b([A-Za-z]+[^s\s])s\b$/, '$1');
156
+ }
157
+ function formatStorageLimit(storageLimitMb) {
158
+ if (storageLimitMb <= 1000)
159
+ return { value: String(storageLimitMb), unit: 'MB' };
160
+ return {
161
+ value: String(Math.round(storageLimitMb / 1000)),
162
+ unit: 'GB'
163
+ };
164
+ }
77
165
  function getFeatureDisplayParts(feature, seatLimit) {
78
166
  const baseLabel = String(feature.publicLabel || feature.label || '').trim() || formatFeatureKeyFallback(feature.featureKey);
79
167
  const config = feature.config && typeof feature.config === 'object' ? feature.config : {};
80
168
  if (feature.featureKey === 'context.uploads') {
81
169
  const storageLimitMb = readConfiguredFeatureLimit(config, 'storageLimitMb');
82
170
  if (storageLimitMb !== null) {
83
- return { limitValue: String(storageLimitMb), limitUnit: 'MB', baseLabel };
171
+ const storageLimit = formatStorageLimit(storageLimitMb);
172
+ return {
173
+ limitValue: storageLimit.value,
174
+ limitUnit: storageLimit.unit,
175
+ baseLabel: formatLimitedFeatureLabel(baseLabel, storageLimitMb)
176
+ };
84
177
  }
85
178
  }
86
179
  if (feature.featureKey === 'workspace.workspaces') {
87
180
  const maxWorkspaces = readConfiguredFeatureLimit(config, 'maxWorkspaces');
88
181
  if (maxWorkspaces !== null) {
89
- return { limitValue: String(maxWorkspaces), limitUnit: 'x', baseLabel };
182
+ return {
183
+ limitValue: String(maxWorkspaces),
184
+ limitUnit: null,
185
+ baseLabel: formatLimitedFeatureLabel(baseLabel, maxWorkspaces)
186
+ };
90
187
  }
91
188
  }
92
189
  if (feature.featureKey === 'collaboration.ai_profiles') {
93
190
  const maxAiProfiles = readConfiguredFeatureLimit(config, 'maxAiProfiles');
94
191
  if (maxAiProfiles !== null) {
95
- return { limitValue: String(maxAiProfiles), limitUnit: 'x', baseLabel };
192
+ return {
193
+ limitValue: String(maxAiProfiles),
194
+ limitUnit: null,
195
+ baseLabel: formatLimitedFeatureLabel(baseLabel, maxAiProfiles)
196
+ };
96
197
  }
97
198
  }
98
199
  if (feature.featureKey === 'collaboration.team_management') {
99
200
  const normalizedSeatLimit = Number(seatLimit);
100
201
  if (Number.isFinite(normalizedSeatLimit) && normalizedSeatLimit > 0) {
101
- return { limitValue: String(Math.floor(normalizedSeatLimit)), limitUnit: 'x', baseLabel };
202
+ const seatLimitValue = Math.floor(normalizedSeatLimit);
203
+ return {
204
+ limitValue: String(seatLimitValue),
205
+ limitUnit: null,
206
+ baseLabel: formatLimitedFeatureLabel(baseLabel, seatLimitValue)
207
+ };
102
208
  }
103
209
  }
104
210
  return { limitValue: null, limitUnit: null, baseLabel };
105
211
  }
106
- export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
212
+ export function PlanComparisonPage({ heading, subtitle, variant = 'app', theme = 'dark', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, preferPricingPageTitle = true, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
107
213
  const [plans, setPlans] = useState([]);
108
214
  const [pricingSource, setPricingSource] = useState(null);
109
215
  const [pricingPageCopy, setPricingPageCopy] = useState({ pageTitle: null, pageDescription: null });
@@ -162,15 +268,16 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
162
268
  return String(a.displayName || a.planId).localeCompare(String(b.displayName || b.planId));
163
269
  }), [plans]);
164
270
  const pricingSourceLabel = useMemo(() => formatPricingSource(pricingSource || fallbackPricingSource), [fallbackPricingSource, pricingSource]);
165
- const resolvedHeading = String(pricingPageCopy.pageTitle || heading).trim() || heading;
271
+ const resolvedHeading = String((preferPricingPageTitle ? pricingPageCopy.pageTitle : null) || heading).trim() || heading;
166
272
  const resolvedSubtitle = String(pricingPageCopy.pageDescription || subtitle || '').trim();
273
+ const resolvedSubtitleHtml = useMemo(() => sanitizePricingDescriptionHtml(resolvedSubtitle), [resolvedSubtitle]);
167
274
  const planSelectionEnabled = authSessionResolved;
168
275
  const contentClassName = `${styles.plansWrapper} ${shellOwnsScroll ? styles.shellOwnsScroll : 'tf-scrollbar'} ${variant === 'site' ? styles.siteVariant : ''} ${chrome ? styles.withChrome : ''}`.trim();
169
276
  const wrapPage = (content) => {
170
277
  if (shellOwnsScroll) {
171
278
  return (_jsxs(_Fragment, { children: [chrome, content, footer] }));
172
279
  }
173
- return (_jsxs("div", { className: taskforceStyles.standaloneWrapper, "data-theme": "dark", children: [chrome, content, footer] }));
280
+ return (_jsxs("div", { className: taskforceStyles.standaloneWrapper, "data-theme": theme, children: [chrome, content, footer] }));
174
281
  };
175
282
  if (loading) {
176
283
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.loader, children: [_jsx(Loader2, { size: 48, className: styles.spinner }), _jsx("p", { children: "Loading plans..." })] }) }) }) }));
@@ -178,82 +285,106 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
178
285
  if (error) {
179
286
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.emptyState, children: [_jsx("h2", { children: "Connecting to plan pricing..." }), _jsx("p", { children: error }), _jsx("button", { className: styles.backBtn, onClick: () => setRetryNonce((value) => value + 1), children: "Retry" })] }) }) }) }));
180
287
  }
181
- return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: resolvedHeading }), resolvedSubtitle && _jsx("p", { className: styles.subtitle, children: resolvedSubtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
288
+ return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: resolvedHeading }), resolvedSubtitleHtml && (_jsx("div", { className: styles.subtitle, dangerouslySetInnerHTML: { __html: resolvedSubtitleHtml } })), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
182
289
  ? styles.successBanner
183
- : (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
184
- const monthPrice = plan.pricing.month;
185
- const yearPrice = plan.pricing.year;
186
- const hasMonth = isSelectablePrice(monthPrice);
187
- const hasYear = isSelectablePrice(yearPrice);
188
- const planVersionId = String(plan.defaultVersion?.planVersionId || '').trim();
189
- const hasActiveEntitlement = isCommercialEntitlementActive(currentEntitlementState);
190
- const shouldMarkCurrentPlan = markCurrentPlan ?? hasActiveEntitlement;
191
- const hasKnownLinkedPlan = Boolean(planVersionId
192
- && currentPlanVersionId
193
- && currentPlanVersionId === planVersionId) || Boolean(!currentPlanVersionId
194
- && Boolean(currentPlanId && currentPlanId === plan.planId));
195
- const isCurrent = shouldMarkCurrentPlan && hasKnownLinkedPlan && currentEntitlementState !== 'canceled';
196
- const defaultInterval = hasMonth ? 'month' : (hasYear ? 'year' : null);
197
- const defaultPrice = defaultInterval === 'year' ? yearPrice : monthPrice;
198
- const currentPlanShowsManageAction = currentPlanCardActionMode === 'manage';
199
- const currentPlanShowsIndicator = currentPlanCardActionMode === 'indicator';
200
- const canSelectCard = planSelectionEnabled
201
- && Boolean(planVersionId)
202
- && (isCurrent
203
- ? (currentPlanShowsManageAction && canManageCurrentPlan)
204
- : Boolean(defaultInterval));
205
- const handleCardSelect = () => {
206
- if (!planVersionId)
207
- return;
208
- if (isCurrent) {
209
- if (!currentPlanShowsManageAction)
210
- return;
211
- if (!canManageCurrentPlan)
212
- return;
213
- void onSelectPlan({
214
- planId: plan.planId,
215
- planVersionId,
216
- interval: defaultInterval || 'month',
217
- pricingType: defaultPrice?.pricingType || 'stripe',
218
- isCurrentPlan: true
219
- });
220
- return;
221
- }
222
- if (!defaultInterval)
223
- return;
224
- void onSelectPlan({
225
- planId: plan.planId,
226
- planVersionId,
227
- interval: defaultInterval,
228
- pricingType: defaultPrice?.pricingType || 'stripe',
229
- isCurrentPlan: false
230
- });
231
- };
232
- return (_jsxs("div", { className: `${styles.planCard} ${isCurrent ? styles.activePlanCard : ''} ${canSelectCard ? styles.clickablePlanCard : ''}`, "data-testid": `plan-card-${plan.planId}`, role: canSelectCard ? 'button' : undefined, tabIndex: canSelectCard ? 0 : undefined, onClick: canSelectCard ? handleCardSelect : undefined, onKeyDown: canSelectCard ? (event) => {
233
- if (event.key === 'Enter' || event.key === ' ') {
234
- event.preventDefault();
235
- handleCardSelect();
236
- }
237
- } : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }), String(plan.description || '').trim() && (_jsx("p", { className: styles.planDescription, children: String(plan.description || '').trim() })), _jsx("div", { className: styles.featuresList, children: Array.isArray(plan.features) && plan.features.length > 0 ? plan.features.map((feature, idx) => (_jsxs("div", { className: styles.featureItem, children: [_jsx(CheckCircle2, { className: styles.featureIcon }), _jsxs("div", { className: styles.featureTextBlock, children: [(() => {
238
- const { limitValue, limitUnit, baseLabel } = getFeatureDisplayParts(feature, plan.defaultVersion?.seatLimit);
239
- return (_jsxs("span", { className: styles.featureText, children: [limitValue && (_jsxs("span", { className: styles.featureLimitAccent, children: [_jsx("span", { children: limitValue }), limitUnit ? (_jsx("span", { className: limitUnit === 'MB' ? styles.featureLimitUnit : undefined, children: limitUnit })) : null] })), _jsx("span", { children: limitValue ? ` ${baseLabel}` : baseLabel })] }));
240
- })(), String(feature.publicDescription || feature.description || '').trim() && (_jsx("span", { className: styles.featureDescription, children: String(feature.publicDescription || feature.description || '').trim() }))] })] }, `${feature.featureKey}-${idx}`))) : (_jsx("span", { className: styles.featureText, children: "No additional features included" })) }), (hasMonth || hasYear) ? (_jsxs("div", { className: styles.priceRows, children: [hasMonth ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(monthPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ month" })] })) : null, hasYear ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(yearPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ year" })] })) : null] })) : null, isCurrent ? (currentPlanShowsManageAction ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-manage-${plan.planId}`, onClick: (event) => {
241
- event.stopPropagation();
290
+ : (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), _jsxs("section", { className: styles.foundingBeta, "aria-labelledby": "founding-beta-heading", children: [_jsx("h2", { id: "founding-beta-heading", children: "Founding beta" }), _jsx("p", { children: "Taskforce is in founding beta. You may run into rough edges while we improve cloud workspaces and agent workflows. Early members get access while the product is still forming, plus a direct role in shaping what comes next." })] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
291
+ const monthPrice = plan.pricing.month;
292
+ const yearPrice = plan.pricing.year;
293
+ const campaignMonthPrice = plan.campaignPricing?.month || null;
294
+ const campaignYearPrice = plan.campaignPricing?.year || null;
295
+ const selectedMonthPricing = getSelectedPricing(monthPrice, campaignMonthPrice);
296
+ const selectedYearPricing = getSelectedPricing(yearPrice, campaignYearPrice);
297
+ const hasMonth = isSelectablePrice(selectedMonthPricing.price);
298
+ const hasYear = isSelectablePrice(selectedYearPricing.price);
299
+ const hasMultipleBillingOptions = hasMonth && hasYear;
300
+ const planVersionId = String(plan.defaultVersion?.planVersionId || '').trim();
301
+ const hasActiveEntitlement = isCommercialEntitlementActive(currentEntitlementState);
302
+ const shouldMarkCurrentPlan = markCurrentPlan ?? hasActiveEntitlement;
303
+ const hasKnownLinkedPlan = Boolean(planVersionId
304
+ && currentPlanVersionId
305
+ && currentPlanVersionId === planVersionId) || Boolean(!currentPlanVersionId
306
+ && Boolean(currentPlanId && currentPlanId === plan.planId));
307
+ const isCurrent = shouldMarkCurrentPlan && hasKnownLinkedPlan && currentEntitlementState !== 'canceled';
308
+ const defaultInterval = hasMonth ? 'month' : (hasYear ? 'year' : null);
309
+ const defaultPricing = defaultInterval === 'year' ? selectedYearPricing : selectedMonthPricing;
310
+ const currentPlanShowsManageAction = currentPlanCardActionMode === 'manage';
311
+ const currentPlanShowsIndicator = currentPlanCardActionMode === 'indicator';
312
+ const canSelectCard = planSelectionEnabled
313
+ && Boolean(planVersionId)
314
+ && (isCurrent
315
+ ? (currentPlanShowsManageAction && canManageCurrentPlan)
316
+ : Boolean(defaultInterval));
317
+ const handleCardSelect = () => {
318
+ if (!planVersionId)
319
+ return;
320
+ if (isCurrent) {
321
+ if (!currentPlanShowsManageAction)
322
+ return;
242
323
  if (!canManageCurrentPlan)
243
324
  return;
244
- void onSelectPlan({ planId: plan.planId, planVersionId, interval: defaultInterval || 'month', pricingType: defaultPrice?.pricingType || 'stripe', isCurrentPlan: true });
245
- }, disabled: !planVersionId || !canManageCurrentPlan || !planSelectionEnabled, children: isAuthenticated
246
- ? (canManageCurrentPlan ? 'Manage Plan' : 'Current Plan')
247
- : 'Current Plan' })) : (currentPlanShowsIndicator ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-current-${plan.planId}`, disabled: true, children: "Current Plan" })) : null)) : (_jsxs("div", { className: styles.ctaRow, children: [hasMonth ? (_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
248
- event.stopPropagation();
249
- void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'month', pricingType: monthPrice?.pricingType || 'stripe', isCurrentPlan: false });
250
- }, disabled: !planVersionId || !planSelectionEnabled, children: monthPrice?.pricingType === 'free'
251
- ? (isAuthenticated ? 'Select Free' : 'Get Started Free')
252
- : (isAuthenticated ? 'Choose Monthly' : 'Get Started Monthly') })) : null, hasYear ? (_jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
325
+ void onSelectPlan({
326
+ planId: plan.planId,
327
+ planVersionId,
328
+ interval: defaultInterval || 'month',
329
+ pricingType: defaultPricing.price?.pricingType || 'stripe',
330
+ pricingAudience: defaultPricing.pricingAudience,
331
+ isCurrentPlan: true
332
+ });
333
+ return;
334
+ }
335
+ if (!defaultInterval)
336
+ return;
337
+ void onSelectPlan({
338
+ planId: plan.planId,
339
+ planVersionId,
340
+ interval: defaultInterval,
341
+ pricingType: defaultPricing.price?.pricingType || 'stripe',
342
+ pricingAudience: defaultPricing.pricingAudience,
343
+ isCurrentPlan: false
344
+ });
345
+ };
346
+ return (_jsxs("div", { className: `${styles.planCard} ${isCurrent ? styles.activePlanCard : ''} ${canSelectCard ? styles.clickablePlanCard : ''}`, "data-testid": `plan-card-${plan.planId}`, role: canSelectCard ? 'button' : undefined, tabIndex: canSelectCard ? 0 : undefined, onClick: canSelectCard ? handleCardSelect : undefined, onKeyDown: canSelectCard ? (event) => {
347
+ if (event.key === 'Enter' || event.key === ' ') {
348
+ event.preventDefault();
349
+ handleCardSelect();
350
+ }
351
+ } : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }), renderPriceRows(monthPrice, yearPrice, campaignMonthPrice, campaignYearPrice), String(plan.description || '').trim() && (_jsx("p", { className: styles.planDescription, children: String(plan.description || '').trim() })), _jsx("div", { className: styles.featuresList, children: Array.isArray(plan.features) && plan.features.length > 0 ? plan.features.map((feature, idx) => (_jsxs("div", { className: styles.featureItem, children: [_jsx(CheckCircle2, { className: styles.featureIcon }), _jsxs("div", { className: styles.featureTextBlock, children: [(() => {
352
+ const { limitValue, limitUnit, baseLabel } = getFeatureDisplayParts(feature, plan.defaultVersion?.seatLimit);
353
+ return (_jsxs("span", { className: styles.featureText, children: [limitValue && (_jsxs("span", { className: styles.featureLimitAccent, children: [_jsx("span", { children: limitValue }), limitUnit ? (_jsx("span", { className: styles.featureLimitUnit, children: limitUnit })) : null] })), _jsx("span", { children: limitValue ? ` ${baseLabel}` : baseLabel })] }));
354
+ })(), feature.publicDescriptionVisible !== false && String(feature.publicDescription || feature.description || '').trim() && (_jsx("span", { className: styles.featureDescription, children: String(feature.publicDescription || feature.description || '').trim() }))] })] }, `${feature.featureKey}-${idx}`))) : (_jsx("span", { className: styles.featureText, children: "No additional features included" })) }), isCurrent ? (currentPlanShowsManageAction ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-manage-${plan.planId}`, onClick: (event) => {
253
355
  event.stopPropagation();
254
- void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'year', pricingType: yearPrice?.pricingType || 'stripe', isCurrentPlan: false });
255
- }, disabled: !planVersionId || !planSelectionEnabled, children: yearPrice?.pricingType === 'free'
256
- ? (isAuthenticated ? 'Select Free' : 'Get Started Free')
257
- : (isAuthenticated ? 'Choose Yearly' : 'Get Started Yearly') })) : null] }))] }, plan.planId));
258
- }) }))] }) }) }));
356
+ if (!canManageCurrentPlan)
357
+ return;
358
+ void onSelectPlan({
359
+ planId: plan.planId,
360
+ planVersionId,
361
+ interval: defaultInterval || 'month',
362
+ pricingType: defaultPricing.price?.pricingType || 'stripe',
363
+ pricingAudience: defaultPricing.pricingAudience,
364
+ isCurrentPlan: true
365
+ });
366
+ }, disabled: !planVersionId || !canManageCurrentPlan || !planSelectionEnabled, children: isAuthenticated
367
+ ? (canManageCurrentPlan ? 'Manage Plan' : 'Current Plan')
368
+ : 'Current Plan' })) : (currentPlanShowsIndicator ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-current-${plan.planId}`, disabled: true, children: "Current Plan" })) : null)) : (_jsxs("div", { className: styles.ctaRow, children: [hasMonth ? (_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
369
+ event.stopPropagation();
370
+ void onSelectPlan({
371
+ planId: plan.planId,
372
+ planVersionId,
373
+ interval: 'month',
374
+ pricingType: selectedMonthPricing.price?.pricingType || 'stripe',
375
+ pricingAudience: selectedMonthPricing.pricingAudience,
376
+ isCurrentPlan: false
377
+ });
378
+ }, disabled: !planVersionId || !planSelectionEnabled, children: hasMultipleBillingOptions ? 'Start Monthly' : 'Get Started' })) : null, hasYear ? (_jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
379
+ event.stopPropagation();
380
+ void onSelectPlan({
381
+ planId: plan.planId,
382
+ planVersionId,
383
+ interval: 'year',
384
+ pricingType: selectedYearPricing.price?.pricingType || 'stripe',
385
+ pricingAudience: selectedYearPricing.pricingAudience,
386
+ isCurrentPlan: false
387
+ });
388
+ }, disabled: !planVersionId || !planSelectionEnabled, children: hasMultipleBillingOptions ? 'Start Yearly' : 'Get Started' })) : null] }))] }, plan.planId));
389
+ }) }), _jsxs("div", { className: styles.pricingNotes, children: [_jsx("p", { children: "Prices are listed in USD. Taxes may apply." }), _jsx("p", { children: "Local install is free on every plan. Cloud limits apply only to hosted workspaces." })] }), _jsxs("section", { className: `${styles.foundingBeta} ${styles.cloudCapacityNote}`, "aria-labelledby": "cloud-capacity-heading", children: [_jsx("h2", { id: "cloud-capacity-heading", children: "Need more cloud capacity?" }), _jsx("p", { children: "Pro users can request expanded limits for cloud workspaces, AI agents, and storage." })] }), _jsxs("div", { className: `${styles.planCard} ${styles.planNoticeCard} ${styles.teamComingSoonCard}`, children: [_jsx("h2", { className: styles.planName, children: "Team workspaces are coming soon." }), _jsx("p", { className: styles.planDescription, children: "Shared workspaces, collaborators, team agent rosters, admin controls, and larger cloud limits are in development." })] })] }))] }) }) }));
259
390
  }