@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.
Files changed (232) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. 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('1x Workspaces');
200
- expect(featureTextNodes).toContain('4x AI Agents');
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('12x Team Management');
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: 'Choose Monthly' })).toBeInTheDocument();
282
- expect(within(operatorCard).queryByRole('button', { name: 'Choose Yearly' })).not.toBeInTheDocument();
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: 'Choose Monthly' })).toBeInTheDocument();
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 Free' })).toBeDisabled();
334
- expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeDisabled();
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 Free' })).toBeEnabled();
339
- expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeEnabled();
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 Free' }));
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.00')).toBeInTheDocument();
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-05-01T12:00:00.000Z'
646
+ trialEnd: '2026-06-01T12:00:00.000Z'
645
647
  })
646
648
  });
647
649
  vi.stubGlobal('fetch', fetchMock);