@taskforcehq/taskforce 0.3.307 → 0.3.309
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.
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +214 -83
- package/dist/components/views/PlanComparisonPage.test.js +249 -14
- package/dist/components/views/PlansPage.js +15 -9
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/core/GlobalSettingsService.js +18 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/PlanEntitlementService.d.ts +20 -0
- package/dist/core/PlanEntitlementService.js +215 -19
- package/dist/core/PlanFeatureCatalog.test.js +55 -0
- package/dist/core/Taskforce.d.ts +14 -0
- package/dist/core/Taskforce.js +6 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/migrations/taskSchemaMigrations.js +37 -15
- package/dist/server/routes/admin.js +146 -5
- package/dist/server/routes/authSupport.d.ts +1 -0
- package/dist/server/routes/authSupport.js +2 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +105 -63
- package/dist/server/routes/billing.test.js +159 -11
- package/dist/server/routes.test.js +91 -10
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +0 -0
- package/dist/ui/agent-logos/chatgpt.png +0 -0
- package/dist/ui/agent-logos/claude-code.jpeg +0 -0
- package/dist/ui/agent-logos/codex.jpeg +0 -0
- package/dist/ui/agent-logos/cursor.png +0 -0
- package/dist/ui/agent-logos/openclaw.jpeg +0 -0
- package/dist/ui/agent-logos/windsurf.png +0 -0
- package/dist/ui/assets/{AgentsModule-2y82_3DO.js → AgentsModule-BS4Pi_nC.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BTLZZCiQ.js → AnnotatedAttachmentWorkspace-CCckzWYZ.js} +1 -1
- package/dist/ui/assets/{ContextAttachmentManager-D2epuNnG.js → ContextAttachmentManager-CfG_ER7C.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-sVa-3Do6.js → DocumentWorkspace-BVaWPA25.js} +1 -1
- package/dist/ui/assets/{EntityActivityTimeline-Zods27y_.js → EntityActivityTimeline-DSj0ThaK.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-D4XseTwW.js → InitiativesModule-AxMkWtxH.js} +1 -1
- package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
- package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
- package/dist/ui/assets/{TaskContextUpload-uC5qx4Bv.js → TaskContextUpload-Dw4rTF4i.js} +1 -1
- package/dist/ui/assets/{TaskSettings-CqvPAsTv.js → TaskSettings-f2ga42_V.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-oOA9bcdK.js → WorkflowsModule-E4UL3mov.js} +1 -1
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/{index-CYyodXsg.css → index-BvAbqx5Q.css} +1 -1
- package/dist/ui/assets/{index-607WPo_X.js → index-MAHKvFqp.js} +4 -4
- package/dist/ui/index.html +2 -2
- package/dist/ui/og-image.png +0 -0
- package/package.json +1 -1
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +0 -1
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +0 -1
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
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, '&')
|
|
26
|
+
.replace(/</g, '<')
|
|
27
|
+
.replace(/>/g, '>')
|
|
28
|
+
.replace(/"/g, '"')
|
|
29
|
+
.replace(/'/g, ''');
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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":
|
|
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 }),
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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({
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
}
|