@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildBacklogDropUpdates, buildScheduleDropResult, buildScheduledDayDropUpdates, buildStatusTransitionUpdates, } from './taskKanbanDropRules';
3
+ const makeTask = (overrides = {}) => ({
4
+ id: 'task-1',
5
+ title: 'Task',
6
+ description: null,
7
+ category: 'default',
8
+ type: 'feature',
9
+ priority: 2,
10
+ status: 'task',
11
+ createdAt: '2026-01-01T00:00:00.000Z',
12
+ ...overrides,
13
+ });
14
+ const toMap = (pairs) => new Map(pairs.map((pair) => [pair.taskId, pair.updates]));
15
+ describe('taskKanbanDropRules', () => {
16
+ it('reorders backlog tasks and recalculates orderInDay', () => {
17
+ const activeTask = makeTask({ id: 'task-2' });
18
+ const result = toMap(buildBacklogDropUpdates({
19
+ activeTask,
20
+ sourceColumnId: 'backlog',
21
+ targetTasks: [makeTask({ id: 'task-1' }), makeTask({ id: 'task-3' })],
22
+ sourceTasks: [makeTask({ id: 'task-1' }), makeTask({ id: 'task-3' })],
23
+ insertAt: 1,
24
+ }));
25
+ expect(result.get('task-1')).toEqual({ orderInDay: 0 });
26
+ expect(result.get('task-2')).toEqual({ orderInDay: 1 });
27
+ expect(result.get('task-3')).toEqual({ orderInDay: 2 });
28
+ });
29
+ it('moving scheduled task to backlog clears scheduled fields', () => {
30
+ const activeTask = makeTask({ id: 'task-2', scheduledDate: '2026-04-14', scheduledWeekKey: '2026-W16' });
31
+ const result = toMap(buildBacklogDropUpdates({
32
+ activeTask,
33
+ sourceColumnId: 'mon',
34
+ targetTasks: [makeTask({ id: 'task-1' })],
35
+ sourceTasks: [],
36
+ insertAt: 1,
37
+ }));
38
+ expect(result.get('task-2')).toEqual({
39
+ scheduledDate: null,
40
+ scheduledWeekKey: null,
41
+ orderInDay: 1,
42
+ });
43
+ });
44
+ it('moving task into a scheduled day sets scheduled fields and reindexes source column', () => {
45
+ const activeTask = makeTask({ id: 'task-3' });
46
+ const result = toMap(buildScheduledDayDropUpdates({
47
+ activeTask,
48
+ sourceColumnId: 'backlog',
49
+ targetColumnId: 'mon',
50
+ targetTasks: [makeTask({ id: 'task-1' })],
51
+ sourceTasks: [makeTask({ id: 'task-2' })],
52
+ insertAt: 1,
53
+ scheduledDate: '2026-04-14',
54
+ scheduledWeekKey: '2026-W16',
55
+ }));
56
+ expect(result.get('task-3')).toEqual({
57
+ scheduledDate: '2026-04-14',
58
+ scheduledWeekKey: '2026-W16',
59
+ orderInDay: 1,
60
+ });
61
+ expect(result.get('task-2')).toEqual({ orderInDay: 0 });
62
+ });
63
+ it('blocks non-terminal tasks from being scheduled into the past', () => {
64
+ const result = buildScheduleDropResult({
65
+ activeTask: makeTask({ id: 'task-3', status: 'task' }),
66
+ sourceColumnId: 'backlog',
67
+ targetColumnId: 'mon',
68
+ targetTasks: [],
69
+ sourceTasks: [],
70
+ overTaskId: null,
71
+ isBelowOverItem: false,
72
+ scheduleDates: {
73
+ mon: '2026-04-13',
74
+ tue: '2026-04-14',
75
+ wed: '2026-04-15',
76
+ thu: '2026-04-16',
77
+ fri: '2026-04-17',
78
+ sat: '2026-04-18',
79
+ sun: '2026-04-19',
80
+ },
81
+ parseToIsoWeekKey: () => '2026-W16',
82
+ todayDate: '2026-04-14',
83
+ });
84
+ expect(result).toEqual({ kind: 'blocked', reason: 'Only completed work can be scheduled in the past.' });
85
+ });
86
+ it('preserves cancelled grouping as a no-op for completed column drops', () => {
87
+ const task = makeTask({ id: 'task-cancelled', status: 'cancelled', completedAt: '2026-04-01T00:00:00.000Z' });
88
+ expect(buildStatusTransitionUpdates(task, 'completed', '2026-04-14T07:00:00.000Z')).toBeNull();
89
+ });
90
+ it('stamps completedAt when moving into completed', () => {
91
+ const nowIso = '2026-04-14T07:00:00.000Z';
92
+ const result = buildStatusTransitionUpdates(makeTask({ id: 'task-done' }), 'completed', nowIso);
93
+ expect(result).toEqual({
94
+ status: 'done',
95
+ inProgress: false,
96
+ readyForReview: false,
97
+ completed: true,
98
+ cancelled: false,
99
+ completedAt: nowIso,
100
+ });
101
+ });
102
+ it('resets review and progress flags when moving back to reviewable workflow states', () => {
103
+ const nowIso = '2026-04-14T07:00:00.000Z';
104
+ const result = buildStatusTransitionUpdates(makeTask({ id: 'task-review', status: 'done', inProgress: true, readyForReview: true, completed: true }), 'review', nowIso);
105
+ expect(result).toEqual({
106
+ status: 'review',
107
+ inProgress: false,
108
+ readyForReview: true,
109
+ completed: false,
110
+ cancelled: false,
111
+ completedAt: null,
112
+ });
113
+ });
114
+ });
@@ -5,7 +5,7 @@ interface ModalProps {
5
5
  title: React.ReactNode;
6
6
  children: React.ReactNode;
7
7
  footer?: React.ReactNode;
8
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
8
+ size?: 'sm' | 'md' | 'mdWide' | 'lg' | 'xl' | 'full';
9
9
  theme?: 'light' | 'dawn' | 'dark' | 'midnight';
10
10
  className?: string;
11
11
  headerActions?: React.ReactNode;
@@ -36,6 +36,7 @@ export function Modal({ isOpen, onClose, title, children, footer, size = 'md', t
36
36
  const sizeClassMap = {
37
37
  sm: styles.modalSizeSm,
38
38
  md: styles.modalSizeMd,
39
+ mdWide: styles.modalSizeMdWide,
39
40
  lg: styles.modalSizeLg,
40
41
  xl: styles.modalSizeXl,
41
42
  full: styles.modalSizeFull
@@ -7,6 +7,10 @@ import favicon from '../../assets/favicon/favicon.svg';
7
7
  import { DEFAULT_TASKFORCE_THEME, isLightThemeFamily } from '../../utils/theme';
8
8
  export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = 'TaskForce', runtimeMode = 'local', theme = DEFAULT_TASKFORCE_THEME, meta, actions }) {
9
9
  const logo = isLightThemeFamily(theme) ? logoDark : logoLight;
10
+ const normalizedProjectName = String(projectName || '').trim();
11
+ const normalizedWorkspaceId = String(currentWorkspaceId || '').trim().toLowerCase();
12
+ const showProjectName = Boolean(normalizedProjectName
13
+ && (runtimeMode !== 'cloud' || (normalizedWorkspaceId && normalizedWorkspaceId !== 'default')));
10
14
  return (_jsxs("div", { className: `${styles.header} ${shellStyles.standaloneHeader}`, children: [_jsxs("div", { className: `${styles.headerTitle} ${shellStyles.standaloneTitle}`, children: [_jsx("img", { src: logo, alt: "Taskforce Logo", className: styles.brandIcon, onError: (event) => {
11
15
  const target = event.currentTarget;
12
16
  if (target.src !== favicon) {
@@ -15,5 +19,5 @@ export function AppShellHeader({ projectName, currentWorkspaceId, brandLabel = '
15
19
  else {
16
20
  target.style.display = 'none';
17
21
  }
18
- } }), _jsx("span", { children: brandLabel }), runtimeMode === 'cloud' && (_jsx("span", { className: styles.brandCloudSuffix, children: "HQ" })), projectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: currentWorkspaceId ? `Workspace ID: ${currentWorkspaceId}` : undefined, children: projectName })] })), meta] }), _jsx("div", { className: styles.headerActions, style: { gap: '16px' }, children: actions })] }));
22
+ } }), _jsx("span", { children: brandLabel }), runtimeMode === 'cloud' && (_jsx("span", { className: styles.brandCloudSuffix, children: "HQ" })), showProjectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: styles.projectSlash, children: "/" }), _jsx("span", { className: styles.projectName, title: currentWorkspaceId ? `Workspace ID: ${currentWorkspaceId}` : undefined, children: normalizedProjectName })] })), meta] }), _jsx("div", { className: styles.headerActions, style: { gap: '16px' }, children: actions })] }));
19
23
  }
@@ -18,4 +18,8 @@ describe('AppShellHeader', () => {
18
18
  render(_jsx(AppShellHeader, { runtimeMode: "local", theme: "dawn" }));
19
19
  expect(screen.getByAltText('Taskforce Logo')).toHaveAttribute('src', logoDark);
20
20
  });
21
+ it('hides a stale project label while cloud runtime is still on the default workspace', () => {
22
+ render(_jsx(AppShellHeader, { runtimeMode: "cloud", projectName: "Old Workspace", currentWorkspaceId: "default" }));
23
+ expect(screen.queryByText('Old Workspace')).not.toBeInTheDocument();
24
+ });
21
25
  });
@@ -58,16 +58,17 @@ interface PlanComparisonPageProps {
58
58
  canManageCurrentPlan?: boolean;
59
59
  pricingEndpoint?: string;
60
60
  statusBanner?: {
61
- type: 'success' | 'error';
61
+ type: 'success' | 'error' | 'info';
62
62
  message: string;
63
63
  } | null;
64
64
  onBack: () => void;
65
65
  backLabel: string;
66
+ backDisabled?: boolean;
66
67
  topActions?: React.ReactNode;
67
68
  shellOwnsScroll?: boolean;
68
69
  showPageBackButton?: boolean;
69
70
  fallbackPricingSource?: PricingSourceDetails | null;
70
71
  onSelectPlan: (intent: PlanSelectionIntent) => void | Promise<void>;
71
72
  }
72
- export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, topActions, shellOwnsScroll, showPageBackButton, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
73
+ export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
73
74
  export {};
@@ -64,7 +64,7 @@ function isActiveEntitlementState(raw) {
64
64
  const value = String(raw || '').trim().toLowerCase();
65
65
  return value === 'active' || value === 'trialing' || value === 'grace';
66
66
  }
67
- export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, topActions, shellOwnsScroll = false, showPageBackButton = true, fallbackPricingSource = null, onSelectPlan }) {
67
+ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, fallbackPricingSource = null, onSelectPlan }) {
68
68
  const [plans, setPlans] = useState([]);
69
69
  const [pricingSource, setPricingSource] = useState(null);
70
70
  const [loading, setLoading] = useState(true);
@@ -130,7 +130,9 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
130
130
  if (error) {
131
131
  return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.emptyState, children: [_jsx("h2", { children: "Connecting to plan pricing..." }), _jsx("p", { children: error }), _jsx("button", { className: styles.backBtn, onClick: () => setRetryNonce((value) => value + 1), children: "Retry" })] }) }) }) }));
132
132
  }
133
- return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [!chrome && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: heading }), subtitle && _jsx("p", { className: styles.subtitle, children: subtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success' ? styles.successBanner : styles.errorBanner}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
133
+ return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [!chrome && (showPageBackButton || topActions) && (_jsxs("div", { className: styles.headerBar, children: [_jsx("div", { children: showPageBackButton ? (_jsx("button", { className: styles.backBtn, onClick: onBack, disabled: backDisabled, children: backLabel })) : null }), _jsx("div", { className: styles.headerActions, children: topActions })] })), _jsx("h1", { className: styles.title, children: heading }), subtitle && _jsx("p", { className: styles.subtitle, children: subtitle }), isDebugModeEnabled() && pricingSourceLabel ? (_jsxs("p", { className: styles.debugMeta, children: ["Pricing source: ", pricingSourceLabel] })) : null, statusBanner && (_jsx("div", { className: `${styles.statusBanner} ${statusBanner.type === 'success'
134
+ ? styles.successBanner
135
+ : (statusBanner.type === 'info' ? styles.infoBanner : styles.errorBanner)}`, children: statusBanner.message }))] }), sortedPlans.length === 0 ? (_jsxs("div", { className: `${styles.emptyState} ${styles.marketingEmptyState}`, children: [_jsx("span", { className: styles.emptyBadge, children: "Coming Soon" }), _jsx("h2", { children: "Paid plans are getting their final polish." }), _jsx("p", { children: "Taskforce pricing is on the way with flexible options for solo operators, teams, and larger rollouts. Check back soon for launch tiers, feature bundles, and early access details." }), _jsxs("div", { className: styles.emptyHighlights, "aria-label": "Upcoming pricing highlights", children: [_jsx("span", { children: "Launch-ready tiers" }), _jsx("span", { children: "Team billing controls" }), _jsx("span", { children: "Feature-based packaging" })] })] })) : (_jsx("div", { className: styles.grid, children: sortedPlans.map((plan) => {
134
136
  const monthPrice = plan.pricing.month;
135
137
  const yearPrice = plan.pricing.year;
136
138
  const hasMonth = isSelectablePrice(monthPrice);
@@ -97,6 +97,10 @@ describe('PlanComparisonPage', () => {
97
97
  expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
98
98
  expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
99
99
  });
100
+ it('can disable the page back button while onboarding remains blocked', async () => {
101
+ render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back to App", backDisabled: true, onSelectPlan: () => { } }));
102
+ expect(await screen.findByRole('button', { name: 'Back to App' })).toBeDisabled();
103
+ });
100
104
  it('selects the whole plan card using the default available interval', async () => {
101
105
  const onSelectPlan = vi.fn();
102
106
  render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
@@ -1,5 +1,7 @@
1
+ import { type AccountProfileSummary } from '../../utils/accountProfileSummaryCache';
1
2
  interface PlansPageProps {
2
3
  isAuthenticated: boolean;
4
+ authUserId?: string | null;
3
5
  runtimeMode: string;
4
6
  authSessionResolved: boolean;
5
7
  currentTheme?: import('../../types').TaskforceTheme;
@@ -7,8 +9,12 @@ interface PlansPageProps {
7
9
  currentWorkspaceId?: string | null;
8
10
  apiBaseUrl?: string | null;
9
11
  connectedEnvironmentSource?: string | null;
12
+ resolveCloudAuthUrl?: (path: string) => string;
10
13
  embedded?: boolean;
11
14
  shellOwnsScroll?: boolean;
15
+ onAccountProfileSummaryChange?: (summary: AccountProfileSummary | null) => void;
16
+ onContinueToTaskforce?: () => void | Promise<void>;
17
+ continueBusy?: boolean;
12
18
  }
13
- export declare function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, currentTheme, projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, embedded, shellOwnsScroll }: PlansPageProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessionResolved, currentTheme, projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, resolveCloudAuthUrl, embedded, shellOwnsScroll, onAccountProfileSummaryChange, onContinueToTaskforce, continueBusy }: PlansPageProps): import("react/jsx-runtime").JSX.Element;
14
20
  export {};
@@ -6,14 +6,19 @@ import styles from './PlansPage.module.css';
6
6
  import { PlanComparisonPage } from './PlanComparisonPage';
7
7
  import { AppShellHeader } from './AppShellHeader';
8
8
  import { resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
9
- import { invalidateBillingStatusCache, loadBillingStatusCached } from '../../utils/billingStatusCache';
9
+ import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from '../../utils/accountProfileSummaryCache';
10
10
  const TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS = 1600;
11
11
  const ACTIVE_ENTITLEMENT_STATES = new Set(['active', 'trialing', 'grace']);
12
12
  const GATE_MESSAGES = {
13
13
  planSelectionRequired: 'Choose a plan to continue.',
14
- misconfigured: 'Signup monetization is misconfigured. Contact support or a system administrator.',
14
+ checkoutPending: 'Complete checkout to continue.',
15
+ misconfigured: 'Onboarding policy is misconfigured. Contact support or a system administrator.',
15
16
  missingEntitlement: 'No entitlement is linked to this account.'
16
17
  };
18
+ const BILLING_CONFLICT_MESSAGES = {
19
+ CHECKOUT_SESSION_EXPIRED: 'Your previous checkout expired. Choose a plan to continue.',
20
+ CHECKOUT_SESSION_SUPERSEDED: 'A newer checkout replaced your previous checkout. Choose a plan to continue.'
21
+ };
17
22
  function parseInterval(raw) {
18
23
  return raw === 'year' ? 'year' : 'month';
19
24
  }
@@ -33,10 +38,10 @@ function buildEmbeddedPlansPath(params = {}) {
33
38
  }
34
39
  return `/?${query.toString()}`;
35
40
  }
36
- export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, currentTheme = 'dark', projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, embedded = false, shellOwnsScroll = false }) {
41
+ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessionResolved, currentTheme = 'dark', projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, resolveCloudAuthUrl, embedded = false, shellOwnsScroll = false, onAccountProfileSummaryChange, onContinueToTaskforce, continueBusy = false }) {
37
42
  const navigate = useNavigate();
38
43
  const location = useLocation();
39
- const [billingStatus, setBillingStatus] = useState(null);
44
+ const [accountProfileSummary, setAccountProfileSummary] = useState(null);
40
45
  const [actionBusy, setActionBusy] = useState(false);
41
46
  const [feedback, setFeedback] = useState(null);
42
47
  const checkoutStartRan = useRef(false);
@@ -63,6 +68,9 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
63
68
  return normalizedPath;
64
69
  return `${normalizedApiBaseUrl}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`;
65
70
  }, [normalizedApiBaseUrl]);
71
+ const buildCloudBillingUrl = useCallback((path) => {
72
+ return resolveCloudAuthUrl ? resolveCloudAuthUrl(path) : buildApiUrl(path);
73
+ }, [buildApiUrl, resolveCloudAuthUrl]);
66
74
  const closeEmbeddedPlansScreen = useCallback(() => {
67
75
  const params = new URLSearchParams(location.search);
68
76
  params.delete('screen');
@@ -82,20 +90,31 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
82
90
  return endpoints;
83
91
  }, [normalizedApiBaseUrl]);
84
92
  const hasResolvedActiveEntitlement = useMemo(() => {
85
- const entitlementState = String(billingStatus?.entitlementState || '').trim().toLowerCase();
93
+ const entitlementState = String(accountProfileSummary?.entitlementState || '').trim().toLowerCase();
86
94
  return ACTIVE_ENTITLEMENT_STATES.has(entitlementState);
87
- }, [billingStatus?.entitlementState]);
95
+ }, [accountProfileSummary?.entitlementState]);
88
96
  const gateMessage = useMemo(() => {
89
97
  if (hasResolvedActiveEntitlement)
90
98
  return null;
91
- if (gateState === 'plan_selection_required')
99
+ const billingConflictCode = String(accountProfileSummary?.billingConflictCode || '').trim().toUpperCase();
100
+ if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
101
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED;
102
+ if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
103
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
104
+ const summaryMessage = String(accountProfileSummary?.message || '').trim();
105
+ if (summaryMessage)
106
+ return summaryMessage;
107
+ const effectiveGateState = String(accountProfileSummary?.gate || gateState || '').trim().toLowerCase();
108
+ if (effectiveGateState === 'plan_selection_required')
92
109
  return GATE_MESSAGES.planSelectionRequired;
93
- if (gateState === 'misconfigured')
110
+ if (effectiveGateState === 'checkout_pending')
111
+ return GATE_MESSAGES.checkoutPending;
112
+ if (effectiveGateState === 'misconfigured')
94
113
  return GATE_MESSAGES.misconfigured;
95
- if (gateState === 'missing_entitlement')
114
+ if (effectiveGateState === 'missing_entitlement')
96
115
  return GATE_MESSAGES.missingEntitlement;
97
116
  return null;
98
- }, [gateState, hasResolvedActiveEntitlement]);
117
+ }, [accountProfileSummary?.billingConflictCode, accountProfileSummary?.gate, accountProfileSummary?.message, gateState, hasResolvedActiveEntitlement]);
99
118
  const clearTransientQueryState = useCallback(() => {
100
119
  const next = new URLSearchParams(location.search);
101
120
  next.delete('checkout');
@@ -116,30 +135,56 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
116
135
  transientQueryClearTimeoutRef.current = null;
117
136
  }, TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS);
118
137
  }, [clearTransientQueryState]);
119
- const loadBillingStatus = useCallback(async () => {
120
- const billingStatusUrl = buildApiUrl('/api/taskforce/billing/status');
138
+ const loadAccountProfileSummary = useCallback(async (options) => {
139
+ const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);
121
140
  if (!isAuthenticated) {
122
- setBillingStatus(null);
123
- invalidateBillingStatusCache(billingStatusUrl);
124
- return;
141
+ setAccountProfileSummary(null);
142
+ invalidateAccountProfileSummaryCache(accountProfileSummaryUrl, { identityKey: authUserId });
143
+ return null;
125
144
  }
126
145
  try {
127
- const payload = await loadBillingStatusCached(billingStatusUrl);
128
- setBillingStatus(payload);
146
+ const payload = await loadAccountProfileSummaryCached(accountProfileSummaryUrl, {
147
+ identityKey: authUserId,
148
+ force: options?.force === true
149
+ });
150
+ setAccountProfileSummary(payload);
151
+ onAccountProfileSummaryChange?.(payload);
152
+ return payload;
129
153
  }
130
154
  catch {
131
155
  // best effort only
156
+ return null;
132
157
  }
133
- }, [buildApiUrl, isAuthenticated]);
158
+ }, [authUserId, buildCloudBillingUrl, isAuthenticated, onAccountProfileSummaryChange]);
159
+ const resolveRecoveryMessage = useCallback((summary, fallback) => {
160
+ const billingConflictCode = String(summary?.billingConflictCode || '').trim().toUpperCase();
161
+ if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
162
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED;
163
+ if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
164
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
165
+ const summaryMessage = String(summary?.message || '').trim();
166
+ if (summaryMessage)
167
+ return summaryMessage;
168
+ return String(fallback || 'Unable to confirm checkout completion.').trim() || 'Unable to confirm checkout completion.';
169
+ }, []);
170
+ const resolveGateFeedbackType = useCallback((message) => {
171
+ const normalized = String(message || '').trim();
172
+ if (normalized === GATE_MESSAGES.planSelectionRequired
173
+ || normalized === GATE_MESSAGES.checkoutPending) {
174
+ return 'info';
175
+ }
176
+ return 'error';
177
+ }, []);
134
178
  const startCheckout = useCallback(async (planVersionId, interval) => {
135
179
  if (!planVersionId) {
136
180
  setFeedback({ type: 'error', message: 'Selected plan is missing a default plan version.' });
137
181
  return;
138
182
  }
183
+ const hadActiveEntitlement = hasResolvedActiveEntitlement;
139
184
  setActionBusy(true);
140
185
  setFeedback(null);
141
186
  try {
142
- const res = await fetch(buildApiUrl('/api/taskforce/billing/checkout-session'), {
187
+ const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/checkout-session'), {
143
188
  method: 'POST',
144
189
  credentials: 'include',
145
190
  headers: { 'Content-Type': 'application/json' },
@@ -153,20 +198,28 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
153
198
  const checkoutState = String(payload?.checkoutState || '').trim().toLowerCase();
154
199
  if (checkoutState === 'free') {
155
200
  setFeedback({ type: 'success', message: 'Plan selected. Your access is ready.' });
156
- invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
157
- await loadBillingStatus();
201
+ invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
202
+ const summary = await loadAccountProfileSummary({ force: true });
203
+ const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
204
+ if (!hadActiveEntitlement && ACTIVE_ENTITLEMENT_STATES.has(summaryState)) {
205
+ await onContinueToTaskforce?.();
206
+ }
158
207
  return;
159
208
  }
160
209
  if (checkoutState === 'updated') {
161
210
  setFeedback({ type: 'success', message: String(payload?.message || 'Your subscription was updated successfully.') });
162
- invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
163
- await loadBillingStatus();
211
+ invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
212
+ const summary = await loadAccountProfileSummary({ force: true });
213
+ const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
214
+ if (!hadActiveEntitlement && ACTIVE_ENTITLEMENT_STATES.has(summaryState)) {
215
+ await onContinueToTaskforce?.();
216
+ }
164
217
  return;
165
218
  }
166
219
  if (checkoutState === 'already_current_plan') {
167
220
  setFeedback({ type: 'success', message: String(payload?.message || 'You are already on this plan.') });
168
- invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
169
- await loadBillingStatus();
221
+ invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
222
+ await loadAccountProfileSummary();
170
223
  return;
171
224
  }
172
225
  const returnBaseUrl = String(payload?.returnBaseUrl || '').trim();
@@ -200,12 +253,16 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
200
253
  finally {
201
254
  setActionBusy(false);
202
255
  }
203
- }, [buildApiUrl, loadBillingStatus]);
256
+ }, [authUserId, buildCloudBillingUrl, hasResolvedActiveEntitlement, loadAccountProfileSummary, onContinueToTaskforce, runtimeMode]);
204
257
  const confirmCheckoutSession = useCallback(async (sessionId) => {
205
- if (!sessionId)
206
- return false;
258
+ if (!sessionId) {
259
+ return {
260
+ confirmed: false,
261
+ errorMessage: 'Checkout session is missing.'
262
+ };
263
+ }
207
264
  try {
208
- const res = await fetch(buildApiUrl('/api/taskforce/billing/checkout-session/confirm'), {
265
+ const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/checkout-session/confirm'), {
209
266
  method: 'POST',
210
267
  credentials: 'include',
211
268
  headers: { 'Content-Type': 'application/json' },
@@ -213,18 +270,26 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
213
270
  });
214
271
  const payload = await res.json().catch(() => ({}));
215
272
  if (!res.ok) {
216
- setFeedback({ type: 'error', message: String(payload?.error || 'Unable to confirm checkout completion.') });
217
- return false;
273
+ return {
274
+ confirmed: false,
275
+ errorMessage: String(payload?.error || 'Unable to confirm checkout completion.')
276
+ };
277
+ }
278
+ invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
279
+ const summary = await loadAccountProfileSummary({ force: true });
280
+ const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
281
+ if (ACTIVE_ENTITLEMENT_STATES.has(summaryState)) {
282
+ await onContinueToTaskforce?.();
218
283
  }
219
- invalidateBillingStatusCache(buildApiUrl('/api/taskforce/billing/status'));
220
- await loadBillingStatus();
221
- return true;
284
+ return { confirmed: true };
222
285
  }
223
286
  catch {
224
- setFeedback({ type: 'error', message: 'Unable to confirm checkout completion.' });
225
- return false;
287
+ return {
288
+ confirmed: false,
289
+ errorMessage: 'Unable to confirm checkout completion.'
290
+ };
226
291
  }
227
- }, [buildApiUrl, loadBillingStatus]);
292
+ }, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, onContinueToTaskforce]);
228
293
  const openBillingPortal = useCallback(async () => {
229
294
  if (!isAuthenticated) {
230
295
  navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`);
@@ -233,7 +298,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
233
298
  setActionBusy(true);
234
299
  setFeedback(null);
235
300
  try {
236
- const res = await fetch(buildApiUrl('/api/taskforce/billing/portal-session'), {
301
+ const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/portal-session'), {
237
302
  method: 'POST',
238
303
  credentials: 'include'
239
304
  });
@@ -255,10 +320,10 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
255
320
  finally {
256
321
  setActionBusy(false);
257
322
  }
258
- }, [buildApiUrl, isAuthenticated, navigate]);
323
+ }, [buildCloudBillingUrl, isAuthenticated, navigate]);
259
324
  useEffect(() => {
260
- void loadBillingStatus();
261
- }, [loadBillingStatus]);
325
+ void loadAccountProfileSummary();
326
+ }, [loadAccountProfileSummary]);
262
327
  useEffect(() => {
263
328
  if (!hasResolvedActiveEntitlement)
264
329
  return;
@@ -276,11 +341,17 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
276
341
  checkoutStartRan.current = true;
277
342
  setFeedback({ type: 'success', message: 'Checkout completed successfully. Finalizing your plan...' });
278
343
  void (async () => {
279
- const confirmed = querySessionId
344
+ const confirmation = querySessionId
280
345
  ? await confirmCheckoutSession(querySessionId)
281
- : false;
282
- if (!confirmed) {
283
- await loadBillingStatus();
346
+ : { confirmed: false, errorMessage: 'Checkout session is missing.' };
347
+ if (!confirmation.confirmed) {
348
+ const summary = await loadAccountProfileSummary({ force: true });
349
+ setFeedback({
350
+ type: 'error',
351
+ message: resolveRecoveryMessage(summary, confirmation.errorMessage)
352
+ });
353
+ scheduleTransientQueryClear();
354
+ return;
284
355
  }
285
356
  setFeedback({ type: 'success', message: 'Checkout completed successfully.' });
286
357
  scheduleTransientQueryClear();
@@ -288,8 +359,14 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
288
359
  return;
289
360
  }
290
361
  if (checkoutState === 'cancel') {
291
- setFeedback({ type: 'error', message: 'Checkout was canceled. No charge was made.' });
292
- scheduleTransientQueryClear();
362
+ void (async () => {
363
+ const summary = await loadAccountProfileSummary({ force: true });
364
+ setFeedback({
365
+ type: 'error',
366
+ message: resolveRecoveryMessage(summary, 'Checkout was canceled. No charge was made.')
367
+ });
368
+ scheduleTransientQueryClear();
369
+ })();
293
370
  return;
294
371
  }
295
372
  if ((checkoutState === 'start' || checkoutState === 'free') && isAuthenticated && queryPlanVersionId && !checkoutStartRan.current) {
@@ -298,7 +375,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
298
375
  return;
299
376
  }
300
377
  if (gateMessage) {
301
- setFeedback({ type: 'error', message: gateMessage });
378
+ setFeedback({ type: resolveGateFeedbackType(gateMessage), message: gateMessage });
302
379
  return;
303
380
  }
304
381
  setFeedback((current) => {
@@ -310,9 +387,13 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
310
387
  return null;
311
388
  if (current.message === GATE_MESSAGES.missingEntitlement)
312
389
  return null;
390
+ if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED)
391
+ return null;
392
+ if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED)
393
+ return null;
313
394
  return current;
314
395
  });
315
- }, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated, loadBillingStatus, queryInterval, queryPlanVersionId, querySessionId, scheduleTransientQueryClear, startCheckout]);
396
+ }, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveGateFeedbackType, resolveRecoveryMessage, scheduleTransientQueryClear, startCheckout]);
316
397
  const handleSelectPlan = useCallback(async (intent) => {
317
398
  if (!isAuthenticated) {
318
399
  const nextCheckoutState = intent.pricingType === 'free' ? 'free' : 'start';
@@ -326,7 +407,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
326
407
  return;
327
408
  }
328
409
  if (intent.isCurrentPlan) {
329
- if (!billingStatus?.stripeCustomerId) {
410
+ if (!accountProfileSummary?.stripeCustomerId) {
330
411
  setFeedback({ type: 'success', message: 'Your current plan does not use Stripe billing.' });
331
412
  return;
332
413
  }
@@ -340,6 +421,10 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
340
421
  await startCheckout(intent.planVersionId, intent.interval);
341
422
  }, [isAuthenticated, navigate, openBillingPortal, startCheckout]);
342
423
  const handleBack = useCallback(() => {
424
+ if (hasResolvedActiveEntitlement && onContinueToTaskforce) {
425
+ void onContinueToTaskforce();
426
+ return;
427
+ }
343
428
  if (embedded) {
344
429
  closeEmbeddedPlansScreen();
345
430
  return;
@@ -349,8 +434,9 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
349
434
  return;
350
435
  }
351
436
  navigate('/');
352
- }, [closeEmbeddedPlansScreen, embedded, navigate]);
353
- const topActions = embedded ? (_jsx(_Fragment, { children: isAuthenticated && billingStatus?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })) })) : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, children: "Back" }), isAuthenticated && billingStatus?.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"] }))] }));
437
+ }, [closeEmbeddedPlansScreen, embedded, hasResolvedActiveEntitlement, navigate, onContinueToTaskforce]);
438
+ const continueAction = hasResolvedActiveEntitlement && onContinueToTaskforce ? (_jsx("button", { className: styles.navAction, onClick: () => { void onContinueToTaskforce(); }, disabled: continueBusy || actionBusy, children: "Take Me to Taskforce" })) : null;
439
+ const topActions = embedded ? (_jsxs(_Fragment, { children: [continueAction, isAuthenticated && accountProfileSummary?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" }))] })) : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, disabled: continueBusy || (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"] }))] }));
354
440
  const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, actions: topActions }));
355
- 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(billingStatus?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(billingStatus?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(billingStatus?.entitlementState || '').trim() || null, canManageCurrentPlan: Boolean(billingStatus?.stripeCustomerId), pricingEndpoint: pricingEndpoints[0], statusBanner: feedback, onBack: handleBack, backLabel: embedded ? 'Back to Board' : 'Back to App', topActions: embedded ? topActions : null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: !embedded, fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
441
+ 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, canManageCurrentPlan: Boolean(accountProfileSummary?.stripeCustomerId), pricingEndpoint: pricingEndpoints[0], statusBanner: feedback, onBack: handleBack, backLabel: hasResolvedActiveEntitlement ? 'Take Me to Taskforce' : (embedded ? 'Back to Board' : 'Back to App'), backDisabled: continueBusy || (isAuthenticated && !hasResolvedActiveEntitlement), topActions: embedded ? topActions : null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: !embedded, fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
356
442
  }
@@ -0,0 +1 @@
1
+ export {};