@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
|
@@ -64,9 +64,122 @@ describe('PlanComparisonPage', () => {
|
|
|
64
64
|
expect(await screen.findByText('Pricing source: production (cloud)')).toBeInTheDocument();
|
|
65
65
|
});
|
|
66
66
|
it('renders plan descriptions when provided by the pricing payload', async () => {
|
|
67
|
-
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
67
|
+
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
68
68
|
expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
|
|
69
69
|
expect(screen.getByText('For small collaborative teams.')).toBeInTheDocument();
|
|
70
|
+
expect(screen.getByRole('heading', { name: 'Founding beta' })).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByText(/Taskforce is in founding beta/)).toBeInTheDocument();
|
|
72
|
+
expect(screen.getByText(/Early members get access while the product is still forming/)).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByText('Prices are listed in USD. Taxes may apply.')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByText('Local install is free on every plan. Cloud limits apply only to hosted workspaces.')).toBeInTheDocument();
|
|
75
|
+
const foundingBeta = screen.getByRole('heading', { name: 'Founding beta' }).closest('section');
|
|
76
|
+
const firstPlanCard = container.querySelector('[data-testid^="plan-card-"]');
|
|
77
|
+
const planCards = container.querySelectorAll('[data-testid^="plan-card-"]');
|
|
78
|
+
const lastPlanCard = planCards[planCards.length - 1];
|
|
79
|
+
const pricingNotes = screen.getByText('Prices are listed in USD. Taxes may apply.').closest('div');
|
|
80
|
+
const cloudCapacity = screen.getByRole('heading', { name: 'Need more cloud capacity?' }).closest('section');
|
|
81
|
+
const teamWorkspacesCard = screen.getByRole('heading', { name: 'Team workspaces are coming soon.' }).closest('div');
|
|
82
|
+
if (!foundingBeta || !firstPlanCard) {
|
|
83
|
+
throw new Error('Expected founding beta copy and a plan card to render.');
|
|
84
|
+
}
|
|
85
|
+
if (!lastPlanCard || !pricingNotes) {
|
|
86
|
+
throw new Error('Expected pricing notes to render below the plan cards.');
|
|
87
|
+
}
|
|
88
|
+
if (!cloudCapacity || !teamWorkspacesCard) {
|
|
89
|
+
throw new Error('Expected cloud capacity copy to render above the team workspaces card.');
|
|
90
|
+
}
|
|
91
|
+
expect(Boolean(foundingBeta.compareDocumentPosition(firstPlanCard) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
92
|
+
expect(Boolean(lastPlanCard.compareDocumentPosition(pricingNotes) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
93
|
+
expect(Boolean(pricingNotes.compareDocumentPosition(cloudCapacity) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
94
|
+
expect(Boolean(cloudCapacity.compareDocumentPosition(teamWorkspacesCard) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it('renders pricing above the plan description', async () => {
|
|
97
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
98
|
+
const teamCard = await screen.findByTestId('plan-card-team-5');
|
|
99
|
+
const monthlyPrice = within(teamCard).getByText('$20');
|
|
100
|
+
const description = within(teamCard).getByText('For small collaborative teams.');
|
|
101
|
+
expect(Boolean(monthlyPrice.compareDocumentPosition(description) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it('shows founding offer pricing when campaign prices are present', async () => {
|
|
104
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => ({
|
|
107
|
+
plans: [
|
|
108
|
+
{
|
|
109
|
+
planId: 'pro',
|
|
110
|
+
displayName: 'Pro',
|
|
111
|
+
description: 'For serious operators.',
|
|
112
|
+
status: 'active',
|
|
113
|
+
defaultVersion: {
|
|
114
|
+
planVersionId: 'pro-v1',
|
|
115
|
+
versionNumber: 1,
|
|
116
|
+
seatLimit: null,
|
|
117
|
+
metadata: {}
|
|
118
|
+
},
|
|
119
|
+
features: [],
|
|
120
|
+
pricing: {
|
|
121
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_pro_month', unitAmount: 3000, currency: 'usd', interval: 'month' },
|
|
122
|
+
year: null
|
|
123
|
+
},
|
|
124
|
+
campaignPricing: {
|
|
125
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_pro_campaign_month', unitAmount: 1200, currency: 'usd', interval: 'month' },
|
|
126
|
+
year: null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
})
|
|
131
|
+
})));
|
|
132
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
133
|
+
const proCard = await screen.findByTestId('plan-card-pro');
|
|
134
|
+
expect(within(proCard).getByText('$30')).toBeInTheDocument();
|
|
135
|
+
expect(within(proCard).getByText('Founding Offer:')).toBeInTheDocument();
|
|
136
|
+
expect(within(proCard).getByText('$12')).toBeInTheDocument();
|
|
137
|
+
expect(within(proCard).getAllByText('/ month')).toHaveLength(2);
|
|
138
|
+
});
|
|
139
|
+
it('renders and selects campaign-only pricing intervals', async () => {
|
|
140
|
+
const onSelectPlan = vi.fn();
|
|
141
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
142
|
+
ok: true,
|
|
143
|
+
json: async () => ({
|
|
144
|
+
plans: [
|
|
145
|
+
{
|
|
146
|
+
planId: 'pro',
|
|
147
|
+
displayName: 'Pro',
|
|
148
|
+
description: 'For serious operators.',
|
|
149
|
+
status: 'active',
|
|
150
|
+
defaultVersion: {
|
|
151
|
+
planVersionId: 'pro-v1',
|
|
152
|
+
versionNumber: 1,
|
|
153
|
+
seatLimit: null,
|
|
154
|
+
metadata: {}
|
|
155
|
+
},
|
|
156
|
+
features: [],
|
|
157
|
+
pricing: {
|
|
158
|
+
month: null,
|
|
159
|
+
year: null
|
|
160
|
+
},
|
|
161
|
+
campaignPricing: {
|
|
162
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_pro_campaign_month', unitAmount: 1200, currency: 'usd', interval: 'month' },
|
|
163
|
+
year: null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
})
|
|
168
|
+
})));
|
|
169
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
170
|
+
const proCard = await screen.findByTestId('plan-card-pro');
|
|
171
|
+
expect(within(proCard).queryByText('$30')).not.toBeInTheDocument();
|
|
172
|
+
expect(within(proCard).getByText('Founding Offer:')).toBeInTheDocument();
|
|
173
|
+
expect(within(proCard).getByText('$12')).toBeInTheDocument();
|
|
174
|
+
fireEvent.click(within(proCard).getByRole('button', { name: 'Get Started' }));
|
|
175
|
+
expect(onSelectPlan).toHaveBeenCalledWith({
|
|
176
|
+
planId: 'pro',
|
|
177
|
+
planVersionId: 'pro-v1',
|
|
178
|
+
interval: 'month',
|
|
179
|
+
pricingType: 'stripe',
|
|
180
|
+
pricingAudience: 'campaign',
|
|
181
|
+
isCurrentPlan: false
|
|
182
|
+
});
|
|
70
183
|
});
|
|
71
184
|
it('prefers pricing payload header copy when provided', async () => {
|
|
72
185
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
@@ -85,6 +198,40 @@ describe('PlanComparisonPage', () => {
|
|
|
85
198
|
expect(await screen.findByRole('heading', { name: 'Choose Your Taskforce Plan' })).toBeInTheDocument();
|
|
86
199
|
expect(screen.getByText('Compare plans and pick the right fit for your team.')).toBeInTheDocument();
|
|
87
200
|
});
|
|
201
|
+
it('can keep the provided heading when pricing payload title exists', async () => {
|
|
202
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
203
|
+
ok: true,
|
|
204
|
+
json: async () => ({
|
|
205
|
+
pageTitle: 'Choose Your Taskforce Plan',
|
|
206
|
+
pricingSource: {
|
|
207
|
+
environment: 'production',
|
|
208
|
+
runtimeMode: 'cloud'
|
|
209
|
+
},
|
|
210
|
+
plans: []
|
|
211
|
+
})
|
|
212
|
+
})));
|
|
213
|
+
render(_jsx(PlanComparisonPage, { heading: "Pricing for human-agent work.", preferPricingPageTitle: false, isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
214
|
+
expect(await screen.findByRole('heading', { name: 'Pricing for human-agent work.' })).toBeInTheDocument();
|
|
215
|
+
expect(screen.queryByRole('heading', { name: 'Choose Your Taskforce Plan' })).not.toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
it('renders pricing payload description as sanitized HTML', async () => {
|
|
218
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
219
|
+
ok: true,
|
|
220
|
+
json: async () => ({
|
|
221
|
+
pageTitle: 'Choose Your Taskforce Plan',
|
|
222
|
+
pageDescription: '<p>Local install is <strong>free</strong>.</p><p><a href="https://example.com/pricing">Read more</a><a href="javascript:alert(1)">Nope</a></p><script>alert(1)</script>',
|
|
223
|
+
plans: []
|
|
224
|
+
})
|
|
225
|
+
})));
|
|
226
|
+
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", subtitle: "Fallback subtitle", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
227
|
+
expect(await screen.findByText(/Local install is/)).toBeInTheDocument();
|
|
228
|
+
expect(container.querySelector('strong')?.textContent).toBe('free');
|
|
229
|
+
expect(container.querySelector('script')).toBeNull();
|
|
230
|
+
expect(screen.queryByText('alert(1)')).not.toBeInTheDocument();
|
|
231
|
+
expect(container.querySelector('a[href="https://example.com/pricing"]')?.textContent).toBe('Read more');
|
|
232
|
+
expect(container.querySelector('a[href^="javascript:"]')).toBeNull();
|
|
233
|
+
expect(screen.getByText('Nope')).toBeInTheDocument();
|
|
234
|
+
});
|
|
88
235
|
it('falls back to provided pricing source details when the pricing response omits them', async () => {
|
|
89
236
|
globalThis.__DEBUG_MODE__ = true;
|
|
90
237
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
@@ -143,7 +290,8 @@ describe('PlanComparisonPage', () => {
|
|
|
143
290
|
access: 'enabled',
|
|
144
291
|
label: 'AI Profiles',
|
|
145
292
|
publicLabel: 'AI teammates',
|
|
146
|
-
publicDescription: 'Add reusable AI teammates to your workspace.'
|
|
293
|
+
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
294
|
+
publicDescriptionVisible: true
|
|
147
295
|
}
|
|
148
296
|
],
|
|
149
297
|
pricing: {
|
|
@@ -159,6 +307,47 @@ describe('PlanComparisonPage', () => {
|
|
|
159
307
|
expect(screen.getByText('Add reusable AI teammates to your workspace.')).toBeInTheDocument();
|
|
160
308
|
expect(screen.queryByText('Collaboration Ai Profiles')).not.toBeInTheDocument();
|
|
161
309
|
});
|
|
310
|
+
it('hides feature descriptions when publicDescriptionVisible is false', async () => {
|
|
311
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
312
|
+
ok: true,
|
|
313
|
+
json: async () => ({
|
|
314
|
+
pricingSource: {
|
|
315
|
+
environment: 'production',
|
|
316
|
+
runtimeMode: 'cloud'
|
|
317
|
+
},
|
|
318
|
+
plans: [
|
|
319
|
+
{
|
|
320
|
+
planId: 'team-5',
|
|
321
|
+
displayName: 'Team 5',
|
|
322
|
+
status: 'active',
|
|
323
|
+
defaultVersion: {
|
|
324
|
+
planVersionId: 'team-5-v2',
|
|
325
|
+
versionNumber: 2,
|
|
326
|
+
seatLimit: 5,
|
|
327
|
+
metadata: {}
|
|
328
|
+
},
|
|
329
|
+
features: [
|
|
330
|
+
{
|
|
331
|
+
featureKey: 'collaboration.ai_profiles',
|
|
332
|
+
access: 'enabled',
|
|
333
|
+
label: 'AI Profiles',
|
|
334
|
+
publicLabel: 'AI teammates',
|
|
335
|
+
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
336
|
+
publicDescriptionVisible: false
|
|
337
|
+
}
|
|
338
|
+
],
|
|
339
|
+
pricing: {
|
|
340
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 2000, currency: 'usd', interval: 'month' },
|
|
341
|
+
year: null
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
})
|
|
346
|
+
})));
|
|
347
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
348
|
+
expect(await screen.findByText('AI teammates')).toBeInTheDocument();
|
|
349
|
+
expect(screen.queryByText('Add reusable AI teammates to your workspace.')).not.toBeInTheDocument();
|
|
350
|
+
});
|
|
162
351
|
it('renders inline numeric feature limits next to each feature label', async () => {
|
|
163
352
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
164
353
|
ok: true,
|
|
@@ -215,10 +404,10 @@ describe('PlanComparisonPage', () => {
|
|
|
215
404
|
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
216
405
|
const freeCard = await screen.findByTestId('plan-card-free');
|
|
217
406
|
const featureTextNodes = Array.from(freeCard.querySelectorAll('span')).map((node) => node.textContent);
|
|
218
|
-
expect(featureTextNodes).toContain('
|
|
219
|
-
expect(featureTextNodes).toContain('
|
|
407
|
+
expect(featureTextNodes).toContain('1 Workspace');
|
|
408
|
+
expect(featureTextNodes).toContain('4 AI Agents');
|
|
220
409
|
expect(featureTextNodes).toContain('24MB Cloud Storage');
|
|
221
|
-
expect(featureTextNodes).toContain('
|
|
410
|
+
expect(featureTextNodes).toContain('12 Team Management');
|
|
222
411
|
expect(container.querySelector('[class*="featureLimitUnit"]')?.textContent).toBe('MB');
|
|
223
412
|
});
|
|
224
413
|
it('renders the storage unit suffix in a smaller accent span', async () => {
|
|
@@ -263,6 +452,48 @@ describe('PlanComparisonPage', () => {
|
|
|
263
452
|
expect(accent?.textContent).toBe('64MB');
|
|
264
453
|
expect(unit?.textContent).toBe('MB');
|
|
265
454
|
});
|
|
455
|
+
it('renders storage limits above 1000 MB as rounded GB', async () => {
|
|
456
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
457
|
+
ok: true,
|
|
458
|
+
json: async () => ({
|
|
459
|
+
pricingSource: {
|
|
460
|
+
environment: 'production',
|
|
461
|
+
runtimeMode: 'cloud'
|
|
462
|
+
},
|
|
463
|
+
plans: [
|
|
464
|
+
{
|
|
465
|
+
planId: 'pro',
|
|
466
|
+
displayName: 'Pro',
|
|
467
|
+
status: 'active',
|
|
468
|
+
defaultVersion: {
|
|
469
|
+
planVersionId: 'pro-v1',
|
|
470
|
+
versionNumber: 1,
|
|
471
|
+
seatLimit: null,
|
|
472
|
+
metadata: {}
|
|
473
|
+
},
|
|
474
|
+
features: [
|
|
475
|
+
{
|
|
476
|
+
featureKey: 'context.uploads',
|
|
477
|
+
access: 'enabled',
|
|
478
|
+
label: 'Cloud Storage',
|
|
479
|
+
config: { storageLimitMb: 2500 }
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
pricing: {
|
|
483
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_pro_month', unitAmount: 1999, currency: 'usd', interval: 'month' },
|
|
484
|
+
year: null
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
]
|
|
488
|
+
})
|
|
489
|
+
})));
|
|
490
|
+
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
491
|
+
await screen.findByText('Cloud Storage');
|
|
492
|
+
const accent = container.querySelector('[class*="featureLimitAccent"]');
|
|
493
|
+
const unit = container.querySelector('[class*="featureLimitUnit"]');
|
|
494
|
+
expect(accent?.textContent).toBe('3GB');
|
|
495
|
+
expect(unit?.textContent).toBe('GB');
|
|
496
|
+
});
|
|
266
497
|
it('hides missing yearly pricing rows and buttons instead of rendering unavailable state', async () => {
|
|
267
498
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
268
499
|
ok: true,
|
|
@@ -297,8 +528,8 @@ describe('PlanComparisonPage', () => {
|
|
|
297
528
|
expect(within(operatorCard).getByText('/ month')).toBeInTheDocument();
|
|
298
529
|
expect(within(operatorCard).queryByText('/ year')).not.toBeInTheDocument();
|
|
299
530
|
expect(within(operatorCard).queryByText('Unavailable')).not.toBeInTheDocument();
|
|
300
|
-
expect(within(operatorCard).getByRole('button', { name: '
|
|
301
|
-
expect(within(operatorCard).queryByRole('button', { name: '
|
|
531
|
+
expect(within(operatorCard).getByRole('button', { name: 'Get Started' })).toBeInTheDocument();
|
|
532
|
+
expect(within(operatorCard).queryByRole('button', { name: 'Start Yearly' })).not.toBeInTheDocument();
|
|
302
533
|
});
|
|
303
534
|
it('does not mark a plan current when only the plan id matches but the version does not', async () => {
|
|
304
535
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v1", currentEntitlementState: "active", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
@@ -338,7 +569,7 @@ describe('PlanComparisonPage', () => {
|
|
|
338
569
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
339
570
|
expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
|
|
340
571
|
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
341
|
-
expect(screen.getByRole('button', { name: '
|
|
572
|
+
expect(screen.getByRole('button', { name: 'Start Monthly' })).toBeInTheDocument();
|
|
342
573
|
});
|
|
343
574
|
it('can disable the page back button while onboarding remains blocked', async () => {
|
|
344
575
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back to App", backDisabled: true, onSelectPlan: () => { } }));
|
|
@@ -349,21 +580,24 @@ describe('PlanComparisonPage', () => {
|
|
|
349
580
|
const view = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
350
581
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
351
582
|
const freeCard = screen.getByTestId('plan-card-free');
|
|
352
|
-
expect(screen.getByRole('button', { name: 'Get Started
|
|
353
|
-
expect(screen.getByRole('button', { name: '
|
|
583
|
+
expect(screen.getByRole('button', { name: 'Get Started' })).toBeDisabled();
|
|
584
|
+
expect(screen.getByRole('button', { name: 'Start Monthly' })).toBeDisabled();
|
|
585
|
+
expect(screen.getByRole('button', { name: 'Start Yearly' })).toBeDisabled();
|
|
354
586
|
expect(freeCard).not.toHaveAttribute('role', 'button');
|
|
355
587
|
view.rerender(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: true, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
356
588
|
const enabledFreeCard = screen.getByTestId('plan-card-free');
|
|
357
|
-
expect(screen.getByRole('button', { name: 'Get Started
|
|
358
|
-
expect(screen.getByRole('button', { name: '
|
|
589
|
+
expect(screen.getByRole('button', { name: 'Get Started' })).toBeEnabled();
|
|
590
|
+
expect(screen.getByRole('button', { name: 'Start Monthly' })).toBeEnabled();
|
|
591
|
+
expect(screen.getByRole('button', { name: 'Start Yearly' })).toBeEnabled();
|
|
359
592
|
expect(enabledFreeCard).toHaveAttribute('role', 'button');
|
|
360
|
-
fireEvent.click(screen.getByRole('button', { name: 'Get Started
|
|
593
|
+
fireEvent.click(screen.getByRole('button', { name: 'Get Started' }));
|
|
361
594
|
await waitFor(() => {
|
|
362
595
|
expect(onSelectPlan).toHaveBeenCalledWith({
|
|
363
596
|
planId: 'free',
|
|
364
597
|
planVersionId: 'free-v1',
|
|
365
598
|
interval: 'month',
|
|
366
599
|
pricingType: 'free',
|
|
600
|
+
pricingAudience: 'public',
|
|
367
601
|
isCurrentPlan: false
|
|
368
602
|
});
|
|
369
603
|
});
|
|
@@ -375,7 +609,7 @@ describe('PlanComparisonPage', () => {
|
|
|
375
609
|
const freeHeading = screen.getByRole('heading', { name: 'Free' });
|
|
376
610
|
const freeCard = freeHeading.closest('[role="button"]');
|
|
377
611
|
expect(freeCard).toBeTruthy();
|
|
378
|
-
expect(within(freeCard).getByText('$0
|
|
612
|
+
expect(within(freeCard).getByText('$0')).toBeInTheDocument();
|
|
379
613
|
expect(within(freeCard).getByText('/ month')).toBeInTheDocument();
|
|
380
614
|
fireEvent.click(freeCard);
|
|
381
615
|
await waitFor(() => {
|
|
@@ -384,6 +618,7 @@ describe('PlanComparisonPage', () => {
|
|
|
384
618
|
planVersionId: 'free-v1',
|
|
385
619
|
interval: 'month',
|
|
386
620
|
pricingType: 'free',
|
|
621
|
+
pricingAudience: 'public',
|
|
387
622
|
isCurrentPlan: false
|
|
388
623
|
});
|
|
389
624
|
});
|
|
@@ -24,6 +24,9 @@ function shouldRetryCheckoutConfirmation(statusCode, errorCode) {
|
|
|
24
24
|
function parseInterval(raw) {
|
|
25
25
|
return raw === 'year' ? 'year' : 'month';
|
|
26
26
|
}
|
|
27
|
+
function parsePricingAudience(raw) {
|
|
28
|
+
return raw === 'campaign' ? 'campaign' : 'public';
|
|
29
|
+
}
|
|
27
30
|
function normalizeIntervalValue(raw) {
|
|
28
31
|
const normalized = String(raw || '').trim().toLowerCase();
|
|
29
32
|
if (normalized === 'year')
|
|
@@ -65,6 +68,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
65
68
|
const queryPlanId = String(query.get('planId') || '').trim();
|
|
66
69
|
const queryPlanVersionId = String(query.get('planVersionId') || '').trim();
|
|
67
70
|
const queryInterval = parseInterval(query.get('interval'));
|
|
71
|
+
const queryPricingAudience = parsePricingAudience(query.get('pricingAudience'));
|
|
68
72
|
const queryIntervalValue = normalizeIntervalValue(query.get('interval'));
|
|
69
73
|
const querySessionId = String(query.get('session_id') || '').trim();
|
|
70
74
|
const normalizedApiBaseUrl = useMemo(() => normalizeApiBaseUrl(apiBaseUrl), [apiBaseUrl]);
|
|
@@ -93,6 +97,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
93
97
|
params.delete('planId');
|
|
94
98
|
params.delete('planVersionId');
|
|
95
99
|
params.delete('interval');
|
|
100
|
+
params.delete('pricingAudience');
|
|
96
101
|
const nextQuery = params.toString();
|
|
97
102
|
navigate(`/${nextQuery ? `?${nextQuery}` : ''}${location.hash || ''}`);
|
|
98
103
|
}, [location.hash, location.search, navigate]);
|
|
@@ -161,7 +166,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
161
166
|
const shouldAutoContinueAfterActivation = useCallback((summary, fallbackGate) => {
|
|
162
167
|
return !resolveCommercialLifecyclePresentation(summary, fallbackGate).allowReturnToApp;
|
|
163
168
|
}, []);
|
|
164
|
-
const startCheckout = useCallback(async (planVersionId, interval, options) => {
|
|
169
|
+
const startCheckout = useCallback(async (planVersionId, interval, pricingAudience = 'public', options) => {
|
|
165
170
|
if (!planVersionId) {
|
|
166
171
|
setFeedback({ type: 'error', message: 'Selected plan is missing a default plan version.' });
|
|
167
172
|
return;
|
|
@@ -173,7 +178,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
173
178
|
method: 'POST',
|
|
174
179
|
credentials: 'include',
|
|
175
180
|
headers: { 'Content-Type': 'application/json' },
|
|
176
|
-
body: JSON.stringify({ planVersionId, interval })
|
|
181
|
+
body: JSON.stringify({ planVersionId, interval, pricingAudience })
|
|
177
182
|
});
|
|
178
183
|
const payload = await res.json().catch(() => ({}));
|
|
179
184
|
if (!res.ok) {
|
|
@@ -504,13 +509,13 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
504
509
|
checkoutStartRan.current = true;
|
|
505
510
|
void (async () => {
|
|
506
511
|
const preCheckoutSummary = await loadAccountProfileSummary({ force: true });
|
|
507
|
-
await startCheckout(queryPlanVersionId, queryInterval, {
|
|
512
|
+
await startCheckout(queryPlanVersionId, queryInterval, queryPricingAudience, {
|
|
508
513
|
autoContinueOnActivation: shouldAutoContinueAfterActivation(preCheckoutSummary, gateState)
|
|
509
514
|
});
|
|
510
515
|
})();
|
|
511
516
|
return;
|
|
512
517
|
}
|
|
513
|
-
}, [authSessionResolved, checkoutState, confirmCheckoutSession, gateState, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveRecoveryMessage, scheduleTransientQueryClear, shouldAutoContinueAfterActivation, startCheckout]);
|
|
518
|
+
}, [authSessionResolved, checkoutState, confirmCheckoutSession, gateState, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, queryPricingAudience, querySessionId, resolveRecoveryMessage, scheduleTransientQueryClear, shouldAutoContinueAfterActivation, startCheckout]);
|
|
514
519
|
useEffect(() => {
|
|
515
520
|
if (checkoutState === 'success' || checkoutState === 'cancel' || checkoutState === 'start' || checkoutState === 'free') {
|
|
516
521
|
return;
|
|
@@ -534,9 +539,10 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
534
539
|
checkout: nextCheckoutState,
|
|
535
540
|
planId: intent.planId,
|
|
536
541
|
planVersionId: intent.planVersionId,
|
|
537
|
-
interval: intent.interval
|
|
542
|
+
interval: intent.interval,
|
|
543
|
+
pricingAudience: intent.pricingAudience
|
|
538
544
|
});
|
|
539
|
-
navigate(`/login?mode=register&planId=${encodeURIComponent(intent.planId)}&planVersionId=${encodeURIComponent(intent.planVersionId)}&interval=${encodeURIComponent(intent.interval)}&next=${encodeURIComponent(nextPath)}`);
|
|
545
|
+
navigate(`/login?mode=register&planId=${encodeURIComponent(intent.planId)}&planVersionId=${encodeURIComponent(intent.planVersionId)}&interval=${encodeURIComponent(intent.interval)}&pricingAudience=${encodeURIComponent(intent.pricingAudience)}&next=${encodeURIComponent(nextPath)}`);
|
|
540
546
|
return;
|
|
541
547
|
}
|
|
542
548
|
if (intent.isCurrentPlan) {
|
|
@@ -549,10 +555,10 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
549
555
|
}
|
|
550
556
|
const autoContinueOnActivation = shouldAutoContinueAfterActivation(accountProfileSummary, gateState);
|
|
551
557
|
if (intent.pricingType === 'free') {
|
|
552
|
-
await startCheckout(intent.planVersionId, intent.interval, { autoContinueOnActivation });
|
|
558
|
+
await startCheckout(intent.planVersionId, intent.interval, intent.pricingAudience, { autoContinueOnActivation });
|
|
553
559
|
return;
|
|
554
560
|
}
|
|
555
|
-
await startCheckout(intent.planVersionId, intent.interval, { autoContinueOnActivation });
|
|
561
|
+
await startCheckout(intent.planVersionId, intent.interval, intent.pricingAudience, { autoContinueOnActivation });
|
|
556
562
|
}, [accountProfileSummary, gateState, isAuthenticated, navigate, openBillingPortal, shouldAutoContinueAfterActivation, startCheckout]);
|
|
557
563
|
const handleBack = useCallback(() => {
|
|
558
564
|
if (hasResolvedActiveEntitlement && onContinueToTaskforce) {
|
|
@@ -571,5 +577,5 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
571
577
|
}, [closeEmbeddedPlansScreen, embedded, handleContinueToTaskforce, hasResolvedActiveEntitlement, navigate, onContinueToTaskforce]);
|
|
572
578
|
const headerActions = embedded ? null : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, disabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), children: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : 'Back' }), isAuthenticated && accountProfileSummary?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })), !isAuthenticated && (_jsxs("button", { className: styles.navAction, onClick: () => navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`), children: [_jsx(User, { size: 16, style: { marginRight: '8px', verticalAlign: 'text-bottom' } }), "Sign In"] }))] }));
|
|
573
579
|
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, onBrandClick: handleBack, actions: headerActions }));
|
|
574
|
-
return (_jsx(PlanComparisonPage, { chrome: chrome, heading: "Subscription Plans", subtitle: "Manage your subscription and explore available features. Your current plan is highlighted below.", isAuthenticated: isAuthenticated, authSessionResolved: authSessionResolved, currentPlanId: String(accountProfileSummary?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(accountProfileSummary?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(accountProfileSummary?.entitlementState || '').trim() || null, markCurrentPlan: lifecyclePresentation.markCurrentPlan, canManageCurrentPlan: Boolean(accountProfileSummary?.stripeCustomerId), pricingEndpoint: pricingEndpoint, statusBanner: feedback, onBack: handleBack, backLabel: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : (embedded ? 'Back to Board' : 'Back to App'), backDisabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), topActions: null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: false, showHeaderBarWithChrome: false, currentPlanCardActionMode: "manage", fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
|
|
580
|
+
return (_jsx(PlanComparisonPage, { chrome: chrome, heading: "Subscription Plans", subtitle: "Manage your subscription and explore available features. Your current plan is highlighted below.", variant: "site", theme: currentTheme, isAuthenticated: isAuthenticated, authSessionResolved: authSessionResolved, currentPlanId: String(accountProfileSummary?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(accountProfileSummary?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(accountProfileSummary?.entitlementState || '').trim() || null, markCurrentPlan: lifecyclePresentation.markCurrentPlan, canManageCurrentPlan: Boolean(accountProfileSummary?.stripeCustomerId), pricingEndpoint: pricingEndpoint, statusBanner: feedback, onBack: handleBack, backLabel: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : (embedded ? 'Back to Board' : 'Back to App'), backDisabled: continueBusy || continueActionBusy || (isAuthenticated && !hasResolvedActiveEntitlement), topActions: null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: false, showHeaderBarWithChrome: false, currentPlanCardActionMode: "manage", fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
|
|
575
581
|
}
|
|
@@ -5,11 +5,12 @@ import { MemoryRouter } from 'react-router-dom';
|
|
|
5
5
|
import { PlansPage } from './PlansPage';
|
|
6
6
|
import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache } from '../../utils/accountProfileSummaryCache';
|
|
7
7
|
vi.mock('./PlanComparisonPage', () => ({
|
|
8
|
-
PlanComparisonPage: ({ heading, pricingEndpoint, statusBanner, topActions, onSelectPlan, onBack, backLabel, backDisabled }) => (_jsxs("div", { children: [_jsx("h1", { children: heading }), pricingEndpoint ? _jsx("div", { "data-testid": "pricing-endpoint", children: pricingEndpoint }) : null, statusBanner?.message ? _jsx("div", { "data-status-type": statusBanner.type, children: statusBanner.message }) : null, topActions, onSelectPlan ? (_jsx("button", { onClick: () => onSelectPlan({
|
|
8
|
+
PlanComparisonPage: ({ heading, variant, pricingEndpoint, statusBanner, topActions, onSelectPlan, onBack, backLabel, backDisabled }) => (_jsxs("div", { children: [_jsx("h1", { children: heading }), variant ? _jsx("div", { "data-testid": "plans-variant", children: variant }) : null, pricingEndpoint ? _jsx("div", { "data-testid": "pricing-endpoint", children: pricingEndpoint }) : null, statusBanner?.message ? _jsx("div", { "data-status-type": statusBanner.type, children: statusBanner.message }) : null, topActions, onSelectPlan ? (_jsx("button", { onClick: () => onSelectPlan({
|
|
9
9
|
planId: 'free',
|
|
10
10
|
planVersionId: 'free-v1',
|
|
11
11
|
interval: 'month',
|
|
12
12
|
pricingType: 'free',
|
|
13
|
+
pricingAudience: 'public',
|
|
13
14
|
isCurrentPlan: false
|
|
14
15
|
}), children: "Select Free Plan" })) : null, onBack ? (_jsx("button", { onClick: onBack, disabled: backDisabled, children: backLabel || 'Back' })) : null] }))
|
|
15
16
|
}));
|
|
@@ -25,6 +26,7 @@ describe('PlansPage', () => {
|
|
|
25
26
|
})));
|
|
26
27
|
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans'], children: _jsx(PlansPage, { isAuthenticated: false, runtimeMode: "local", authSessionResolved: true, apiBaseUrl: "https://performance-app.taskforcehq.ai/api", resolveCloudAuthUrl: (path) => `/api/proxy?path=${encodeURIComponent(path)}` }) }));
|
|
27
28
|
expect(await screen.findByTestId('pricing-endpoint')).toHaveTextContent('/api/proxy?path=%2Fapi%2Ftaskforce%2Fpublic%2Fpricing');
|
|
29
|
+
expect(screen.getByTestId('plans-variant')).toHaveTextContent('site');
|
|
28
30
|
});
|
|
29
31
|
it('shows recovery messaging only after bounded retries are exhausted', async () => {
|
|
30
32
|
vi.useFakeTimers();
|
|
@@ -641,7 +643,7 @@ describe('PlansPage', () => {
|
|
|
641
643
|
planId: 'starter',
|
|
642
644
|
planVersionId: 'starter-v1',
|
|
643
645
|
gate: 'ok',
|
|
644
|
-
trialEnd: '2026-
|
|
646
|
+
trialEnd: '2026-06-01T12:00:00.000Z'
|
|
645
647
|
})
|
|
646
648
|
});
|
|
647
649
|
vi.stubGlobal('fetch', fetchMock);
|
|
@@ -200,6 +200,15 @@ export function getGlobalSettingsService(deps) {
|
|
|
200
200
|
: {}),
|
|
201
201
|
...(global.site.siteTheme === 'light' || global.site.siteTheme === 'dark'
|
|
202
202
|
? { siteTheme: global.site.siteTheme }
|
|
203
|
+
: {}),
|
|
204
|
+
...(typeof global.site.pricingPageTitle === 'string'
|
|
205
|
+
? { pricingPageTitle: global.site.pricingPageTitle.trim() }
|
|
206
|
+
: {}),
|
|
207
|
+
...(typeof global.site.pricingPageDescription === 'string'
|
|
208
|
+
? { pricingPageDescription: global.site.pricingPageDescription.trim() }
|
|
209
|
+
: {}),
|
|
210
|
+
...(typeof global.site.pricingPageShowPublicDescriptions === 'boolean'
|
|
211
|
+
? { pricingPageShowPublicDescriptions: global.site.pricingPageShowPublicDescriptions }
|
|
203
212
|
: {})
|
|
204
213
|
}
|
|
205
214
|
}
|
|
@@ -419,6 +428,15 @@ export function updateGlobalSettingsService(deps, settings) {
|
|
|
419
428
|
: {}),
|
|
420
429
|
...(incoming.siteTheme === 'light' || incoming.siteTheme === 'dark'
|
|
421
430
|
? { siteTheme: incoming.siteTheme }
|
|
431
|
+
: {}),
|
|
432
|
+
...(typeof incoming.pricingPageTitle === 'string'
|
|
433
|
+
? { pricingPageTitle: incoming.pricingPageTitle.trim() }
|
|
434
|
+
: {}),
|
|
435
|
+
...(typeof incoming.pricingPageDescription === 'string'
|
|
436
|
+
? { pricingPageDescription: incoming.pricingPageDescription.trim() }
|
|
437
|
+
: {}),
|
|
438
|
+
...(typeof incoming.pricingPageShowPublicDescriptions === 'boolean'
|
|
439
|
+
? { pricingPageShowPublicDescriptions: incoming.pricingPageShowPublicDescriptions }
|
|
422
440
|
: {})
|
|
423
441
|
};
|
|
424
442
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { TaskforceCore } from './Taskforce';
|
|
6
|
+
const roots = [];
|
|
7
|
+
function makeProjectRoot() {
|
|
8
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-global-settings-'));
|
|
9
|
+
roots.push(root);
|
|
10
|
+
return root;
|
|
11
|
+
}
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
while (roots.length > 0) {
|
|
14
|
+
const root = roots.pop();
|
|
15
|
+
if (root)
|
|
16
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
describe('TaskforceCore global site settings', () => {
|
|
20
|
+
it('persists pricing page title and description', () => {
|
|
21
|
+
const projectRoot = makeProjectRoot();
|
|
22
|
+
const storagePath = '.taskforce';
|
|
23
|
+
const core = new TaskforceCore({ projectRoot, storagePath });
|
|
24
|
+
core.updateGlobalSettings({
|
|
25
|
+
site: {
|
|
26
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
27
|
+
pricingPageDescription: 'Compare plans and pick the right fit.',
|
|
28
|
+
pricingPageShowPublicDescriptions: false
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
expect(core.getGlobalSettings().site).toMatchObject({
|
|
32
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
33
|
+
pricingPageDescription: 'Compare plans and pick the right fit.',
|
|
34
|
+
pricingPageShowPublicDescriptions: false
|
|
35
|
+
});
|
|
36
|
+
core.close();
|
|
37
|
+
const reopened = new TaskforceCore({ projectRoot, storagePath });
|
|
38
|
+
expect(reopened.getGlobalSettings().site).toMatchObject({
|
|
39
|
+
pricingPageTitle: 'Choose Your Plan',
|
|
40
|
+
pricingPageDescription: 'Compare plans and pick the right fit.',
|
|
41
|
+
pricingPageShowPublicDescriptions: false
|
|
42
|
+
});
|
|
43
|
+
reopened.close();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -20,6 +20,14 @@ export declare class PlanEntitlementService {
|
|
|
20
20
|
private normalizeOptionalCatalogMarketingCopy;
|
|
21
21
|
private canonicalizePlanFeatureKey;
|
|
22
22
|
private normalizeOptionalCatalogDisplayOrder;
|
|
23
|
+
private normalizeOptionalCatalogDescriptionVisible;
|
|
24
|
+
private isBuiltInPlanFeatureKey;
|
|
25
|
+
private normalizeRequiredCatalogLabel;
|
|
26
|
+
private slugifyCustomPlanFeatureLabel;
|
|
27
|
+
private featureKeyExists;
|
|
28
|
+
private generateUniqueCustomPlanFeatureKey;
|
|
29
|
+
private listCustomPlanFeatureCatalogEntries;
|
|
30
|
+
private getCustomPlanFeatureCatalogEntry;
|
|
23
31
|
private listPlanFeatureCatalogOverrideMap;
|
|
24
32
|
normalizeEntitlementState(value: unknown): EntitlementState;
|
|
25
33
|
private normalizeFeatureAccess;
|
|
@@ -171,10 +179,22 @@ export declare class PlanEntitlementService {
|
|
|
171
179
|
};
|
|
172
180
|
listPlanCatalog(): PlanCatalogSnapshot[];
|
|
173
181
|
listPlanFeatureCatalog(): PlanFeatureCatalogItem[];
|
|
182
|
+
createCustomPlanFeatureCatalogEntry(input: {
|
|
183
|
+
label: string;
|
|
184
|
+
description?: string | null;
|
|
185
|
+
publicLabel?: string | null;
|
|
186
|
+
publicDescription?: string | null;
|
|
187
|
+
publicDescriptionVisible?: boolean | null;
|
|
188
|
+
publicDisplayOrder?: number | null;
|
|
189
|
+
}): PlanFeatureCatalogItem;
|
|
190
|
+
deleteCustomPlanFeatureCatalogEntry(featureKeyRaw: unknown): void;
|
|
174
191
|
updatePlanFeatureCatalogMarketingCopy(input: {
|
|
175
192
|
featureKey: string;
|
|
193
|
+
label?: string | null;
|
|
194
|
+
description?: string | null;
|
|
176
195
|
publicLabel?: string | null;
|
|
177
196
|
publicDescription?: string | null;
|
|
197
|
+
publicDescriptionVisible?: boolean | null;
|
|
178
198
|
publicDisplayOrder?: number | null;
|
|
179
199
|
}): PlanFeatureCatalogItem;
|
|
180
200
|
listPlanVersions(planId: string): PlanVersionSnapshot[];
|