@taskforcehq/taskforce 0.3.306 → 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.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +317 -27
- package/dist/TaskforceCore.js +1 -0
- package/dist/TaskforceCore.test.js +178 -0
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.js +15 -3
- package/dist/components/features/AgentsModule.d.ts +5 -1
- package/dist/components/features/AgentsModule.js +142 -39
- package/dist/components/features/AgentsModule.test.js +260 -11
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +3 -1
- package/dist/components/features/DocumentViewer.js +6 -5
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +20 -3
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +6 -2
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +14 -64
- package/dist/components/task/TaskKanban.js +0 -8
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +20 -15
- package/dist/components/views/PlanComparisonPage.js +221 -82
- package/dist/components/views/PlanComparisonPage.test.js +268 -14
- package/dist/components/views/PlansPage.js +16 -10
- package/dist/components/views/PlansPage.test.js +4 -2
- package/dist/components/views/StandaloneLayout.js +256 -69
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
- package/dist/components/views/panels/PlanningDrawer.js +259 -51
- package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +7 -0
- package/dist/core/AiProfileService.js +460 -35
- package/dist/core/AiProfiles.test.js +648 -2
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +58 -13
- package/dist/core/GlobalSettingsService.js +30 -0
- package/dist/core/GlobalSettingsService.test.d.ts +1 -0
- package/dist/core/GlobalSettingsService.test.js +45 -0
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- 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/PlanningEntities.test.js +366 -4
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +204 -26
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +781 -71
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
- package/dist/hooks/sync/orchestratorShared.js +61 -1
- package/dist/hooks/sync/recovery.js +8 -0
- package/dist/hooks/sync/recovery.test.js +5 -5
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +36 -9
- package/dist/hooks/sync/transfers.test.js +534 -2
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.js +50 -5
- package/dist/hooks/useTaskforce.d.ts +85 -50
- package/dist/hooks/useTaskforce.js +44 -14
- package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
- package/dist/hooks/useWorkspaceSyncController.js +7 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +113 -7
- package/dist/mcp/runtime.d.ts +1 -0
- package/dist/mcp/runtime.js +969 -122
- package/dist/mcp/runtime.test.js +1083 -11
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.js +58 -15
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
- package/dist/server/index.js +4 -0
- package/dist/server/index.test.js +6 -1
- package/dist/server/routes/admin.js +328 -9
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +11 -0
- package/dist/server/routes/auth.js +189 -27
- package/dist/server/routes/authSupport.d.ts +5 -0
- package/dist/server/routes/authSupport.js +13 -1
- package/dist/server/routes/billing.d.ts +1 -0
- package/dist/server/routes/billing.js +116 -63
- package/dist/server/routes/billing.test.js +162 -11
- package/dist/server/routes/documents.js +10 -0
- package/dist/server/routes/shared.js +0 -10
- package/dist/server/routes/sync.integration.test.js +221 -0
- package/dist/server/routes/sync.js +13 -3
- package/dist/server/routes/syncSnapshotRoutes.js +2 -0
- package/dist/server/routes/tasks.js +12 -9
- package/dist/server/routes.js +87 -8
- package/dist/server/routes.test.js +972 -23
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/sync/collaborationSyncPayload.js +2 -0
- package/dist/sync/collaborationSyncPayload.test.js +1 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +5 -1
- package/dist/sync/contentSyncState.js +231 -37
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +4 -4
- package/dist/sync/syncApplyHandlers.js +89 -31
- package/dist/sync/syncApplyHandlers.test.js +184 -4
- package/dist/sync/syncResourceAdapters.d.ts +2 -0
- package/dist/sync/syncResourceAdapters.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +13 -1
- package/dist/sync/syncService.test.js +39 -0
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +129 -70
- package/dist/sync/workspacePullFeed.test.js +49 -0
- package/dist/sync/workspaceSyncModel.d.ts +28 -6
- package/dist/sync/workspaceSyncModel.js +159 -45
- package/dist/sync/workspaceSyncModel.test.js +269 -3
- package/dist/types.d.ts +33 -2
- package/dist/ui/agent-logos/ChatGPT.png +0 -0
- package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
- package/dist/ui/agent-logos/antigravity.jpeg +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-BS4Pi_nC.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
- package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
- package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/{InitiativesModule-9E6ParrY.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-Dw4rTF4i.js +1 -0
- package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
- package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
- package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
- package/dist/ui/assets/index-MAHKvFqp.js +4 -0
- package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/og-image.png +0 -0
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +6 -0
- package/dist/utils/assignees.js +8 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/taskActivity.d.ts +7 -1
- package/dist/utils/taskActivity.js +63 -1
- package/dist/utils/taskActivity.test.js +23 -1
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/package.json +1 -1
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
- package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
- package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
- package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
- package/dist/ui/assets/index-C6k75jr8.js +0 -6
|
@@ -9,6 +9,8 @@ describe('PlanComparisonPage', () => {
|
|
|
9
9
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
10
10
|
ok: true,
|
|
11
11
|
json: async () => ({
|
|
12
|
+
pageTitle: 'Subscription Plans',
|
|
13
|
+
pageDescription: 'Manage your subscription and explore available features. Your current plan is highlighted below.',
|
|
12
14
|
pricingSource: {
|
|
13
15
|
environment: 'production',
|
|
14
16
|
runtimeMode: 'cloud'
|
|
@@ -62,9 +64,173 @@ describe('PlanComparisonPage', () => {
|
|
|
62
64
|
expect(await screen.findByText('Pricing source: production (cloud)')).toBeInTheDocument();
|
|
63
65
|
});
|
|
64
66
|
it('renders plan descriptions when provided by the pricing payload', async () => {
|
|
65
|
-
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: () => { } }));
|
|
66
68
|
expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
|
|
67
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
|
+
});
|
|
183
|
+
});
|
|
184
|
+
it('prefers pricing payload header copy when provided', async () => {
|
|
185
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
186
|
+
ok: true,
|
|
187
|
+
json: async () => ({
|
|
188
|
+
pageTitle: 'Choose Your Taskforce Plan',
|
|
189
|
+
pageDescription: 'Compare plans and pick the right fit for your team.',
|
|
190
|
+
pricingSource: {
|
|
191
|
+
environment: 'production',
|
|
192
|
+
runtimeMode: 'cloud'
|
|
193
|
+
},
|
|
194
|
+
plans: []
|
|
195
|
+
})
|
|
196
|
+
})));
|
|
197
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", subtitle: "Fallback subtitle", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
198
|
+
expect(await screen.findByRole('heading', { name: 'Choose Your Taskforce Plan' })).toBeInTheDocument();
|
|
199
|
+
expect(screen.getByText('Compare plans and pick the right fit for your team.')).toBeInTheDocument();
|
|
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();
|
|
68
234
|
});
|
|
69
235
|
it('falls back to provided pricing source details when the pricing response omits them', async () => {
|
|
70
236
|
globalThis.__DEBUG_MODE__ = true;
|
|
@@ -124,7 +290,8 @@ describe('PlanComparisonPage', () => {
|
|
|
124
290
|
access: 'enabled',
|
|
125
291
|
label: 'AI Profiles',
|
|
126
292
|
publicLabel: 'AI teammates',
|
|
127
|
-
publicDescription: 'Add reusable AI teammates to your workspace.'
|
|
293
|
+
publicDescription: 'Add reusable AI teammates to your workspace.',
|
|
294
|
+
publicDescriptionVisible: true
|
|
128
295
|
}
|
|
129
296
|
],
|
|
130
297
|
pricing: {
|
|
@@ -140,6 +307,47 @@ describe('PlanComparisonPage', () => {
|
|
|
140
307
|
expect(screen.getByText('Add reusable AI teammates to your workspace.')).toBeInTheDocument();
|
|
141
308
|
expect(screen.queryByText('Collaboration Ai Profiles')).not.toBeInTheDocument();
|
|
142
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
|
+
});
|
|
143
351
|
it('renders inline numeric feature limits next to each feature label', async () => {
|
|
144
352
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
145
353
|
ok: true,
|
|
@@ -196,10 +404,10 @@ describe('PlanComparisonPage', () => {
|
|
|
196
404
|
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
197
405
|
const freeCard = await screen.findByTestId('plan-card-free');
|
|
198
406
|
const featureTextNodes = Array.from(freeCard.querySelectorAll('span')).map((node) => node.textContent);
|
|
199
|
-
expect(featureTextNodes).toContain('
|
|
200
|
-
expect(featureTextNodes).toContain('
|
|
407
|
+
expect(featureTextNodes).toContain('1 Workspace');
|
|
408
|
+
expect(featureTextNodes).toContain('4 AI Agents');
|
|
201
409
|
expect(featureTextNodes).toContain('24MB Cloud Storage');
|
|
202
|
-
expect(featureTextNodes).toContain('
|
|
410
|
+
expect(featureTextNodes).toContain('12 Team Management');
|
|
203
411
|
expect(container.querySelector('[class*="featureLimitUnit"]')?.textContent).toBe('MB');
|
|
204
412
|
});
|
|
205
413
|
it('renders the storage unit suffix in a smaller accent span', async () => {
|
|
@@ -244,6 +452,48 @@ describe('PlanComparisonPage', () => {
|
|
|
244
452
|
expect(accent?.textContent).toBe('64MB');
|
|
245
453
|
expect(unit?.textContent).toBe('MB');
|
|
246
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
|
+
});
|
|
247
497
|
it('hides missing yearly pricing rows and buttons instead of rendering unavailable state', async () => {
|
|
248
498
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
249
499
|
ok: true,
|
|
@@ -278,8 +528,8 @@ describe('PlanComparisonPage', () => {
|
|
|
278
528
|
expect(within(operatorCard).getByText('/ month')).toBeInTheDocument();
|
|
279
529
|
expect(within(operatorCard).queryByText('/ year')).not.toBeInTheDocument();
|
|
280
530
|
expect(within(operatorCard).queryByText('Unavailable')).not.toBeInTheDocument();
|
|
281
|
-
expect(within(operatorCard).getByRole('button', { name: '
|
|
282
|
-
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();
|
|
283
533
|
});
|
|
284
534
|
it('does not mark a plan current when only the plan id matches but the version does not', async () => {
|
|
285
535
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v1", currentEntitlementState: "active", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
@@ -319,7 +569,7 @@ describe('PlanComparisonPage', () => {
|
|
|
319
569
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
320
570
|
expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
|
|
321
571
|
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
322
|
-
expect(screen.getByRole('button', { name: '
|
|
572
|
+
expect(screen.getByRole('button', { name: 'Start Monthly' })).toBeInTheDocument();
|
|
323
573
|
});
|
|
324
574
|
it('can disable the page back button while onboarding remains blocked', async () => {
|
|
325
575
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back to App", backDisabled: true, onSelectPlan: () => { } }));
|
|
@@ -330,21 +580,24 @@ describe('PlanComparisonPage', () => {
|
|
|
330
580
|
const view = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
331
581
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
332
582
|
const freeCard = screen.getByTestId('plan-card-free');
|
|
333
|
-
expect(screen.getByRole('button', { name: 'Get Started
|
|
334
|
-
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();
|
|
335
586
|
expect(freeCard).not.toHaveAttribute('role', 'button');
|
|
336
587
|
view.rerender(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: true, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
337
588
|
const enabledFreeCard = screen.getByTestId('plan-card-free');
|
|
338
|
-
expect(screen.getByRole('button', { name: 'Get Started
|
|
339
|
-
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();
|
|
340
592
|
expect(enabledFreeCard).toHaveAttribute('role', 'button');
|
|
341
|
-
fireEvent.click(screen.getByRole('button', { name: 'Get Started
|
|
593
|
+
fireEvent.click(screen.getByRole('button', { name: 'Get Started' }));
|
|
342
594
|
await waitFor(() => {
|
|
343
595
|
expect(onSelectPlan).toHaveBeenCalledWith({
|
|
344
596
|
planId: 'free',
|
|
345
597
|
planVersionId: 'free-v1',
|
|
346
598
|
interval: 'month',
|
|
347
599
|
pricingType: 'free',
|
|
600
|
+
pricingAudience: 'public',
|
|
348
601
|
isCurrentPlan: false
|
|
349
602
|
});
|
|
350
603
|
});
|
|
@@ -356,7 +609,7 @@ describe('PlanComparisonPage', () => {
|
|
|
356
609
|
const freeHeading = screen.getByRole('heading', { name: 'Free' });
|
|
357
610
|
const freeCard = freeHeading.closest('[role="button"]');
|
|
358
611
|
expect(freeCard).toBeTruthy();
|
|
359
|
-
expect(within(freeCard).getByText('$0
|
|
612
|
+
expect(within(freeCard).getByText('$0')).toBeInTheDocument();
|
|
360
613
|
expect(within(freeCard).getByText('/ month')).toBeInTheDocument();
|
|
361
614
|
fireEvent.click(freeCard);
|
|
362
615
|
await waitFor(() => {
|
|
@@ -365,6 +618,7 @@ describe('PlanComparisonPage', () => {
|
|
|
365
618
|
planVersionId: 'free-v1',
|
|
366
619
|
interval: 'month',
|
|
367
620
|
pricingType: 'free',
|
|
621
|
+
pricingAudience: 'public',
|
|
368
622
|
isCurrentPlan: false
|
|
369
623
|
});
|
|
370
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) {
|
|
@@ -570,6 +576,6 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
570
576
|
navigate('/');
|
|
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
|
-
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, 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 }));
|
|
579
|
+
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, onBrandClick: handleBack, actions: headerActions }));
|
|
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);
|