@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.
- package/dist/Taskforce.module.css +113 -5
- package/dist/TaskforceCore.js +560 -208
- package/dist/TaskforceCore.test.js +1833 -91
- package/dist/annotatedAttachments/service.d.ts +1 -1
- package/dist/annotatedAttachments/types.d.ts +1 -1
- package/dist/components/features/AgentsModule.js +1 -1
- package/dist/components/features/AgentsModule.test.js +1 -1
- package/dist/components/features/TaskSettings.js +58 -38
- package/dist/components/features/TaskSettings.test.js +139 -40
- package/dist/components/task/TaskActionHeader.d.ts +2 -1
- package/dist/components/task/TaskActionHeader.js +3 -3
- package/dist/components/task/TaskActionHeader.test.js +8 -0
- package/dist/components/task/TaskCard.js +16 -1
- package/dist/components/task/TaskCard.test.js +31 -0
- package/dist/components/task/TaskForm.d.ts +2 -1
- package/dist/components/task/TaskForm.js +22 -4
- package/dist/components/task/TaskForm.test.js +46 -0
- package/dist/components/task/TaskKanban.d.ts +1 -1
- package/dist/components/task/TaskKanban.js +31 -160
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/task/TaskStatusActions.d.ts +6 -1
- package/dist/components/task/TaskStatusActions.js +8 -3
- package/dist/components/task/TaskStatusActions.test.js +9 -0
- package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
- package/dist/components/task/taskKanbanDropRules.js +187 -0
- package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
- package/dist/components/task/taskKanbanDropRules.test.js +114 -0
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.js +1 -0
- package/dist/components/views/AppShellHeader.js +5 -1
- package/dist/components/views/AppShellHeader.test.js +4 -0
- package/dist/components/views/PlanComparisonPage.d.ts +3 -2
- package/dist/components/views/PlanComparisonPage.js +4 -2
- package/dist/components/views/PlanComparisonPage.test.js +4 -0
- package/dist/components/views/PlansPage.d.ts +7 -1
- package/dist/components/views/PlansPage.js +139 -53
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +157 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +216 -186
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
- package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +31 -5
- package/dist/core/PlanEntitlementService.js +350 -222
- package/dist/core/SignupMonetizationSettings.test.js +208 -49
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskActivityService.d.ts +4 -1
- package/dist/core/TaskActivityService.js +45 -0
- package/dist/core/TaskAuditTimeline.test.js +52 -1
- package/dist/core/Taskforce.d.ts +65 -20
- package/dist/core/Taskforce.js +261 -51
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- package/dist/core/types.d.ts +11 -12
- package/dist/documentReviews/service.d.ts +1 -1
- package/dist/documentReviews/types.d.ts +1 -1
- package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
- package/dist/hooks/sync/auth.d.ts +37 -0
- package/dist/hooks/sync/auth.js +61 -0
- package/dist/hooks/sync/bootstrap.d.ts +34 -0
- package/dist/hooks/sync/bootstrap.js +52 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
- package/dist/hooks/sync/orchestratorShared.js +209 -0
- package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
- package/dist/hooks/sync/orchestratorShared.test.js +49 -0
- package/dist/hooks/sync/realtime.d.ts +25 -0
- package/dist/hooks/sync/realtime.js +60 -0
- package/dist/hooks/sync/recovery.d.ts +69 -0
- package/dist/hooks/sync/recovery.js +118 -0
- package/dist/hooks/sync/transfers.d.ts +122 -0
- package/dist/hooks/sync/transfers.js +431 -0
- package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
- package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
- package/dist/hooks/ui/useResourceExport.d.ts +3 -1
- package/dist/hooks/ui/useResourceExport.js +5 -2
- package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
- package/dist/hooks/useRealtimeSync.js +2 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
- package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
- package/dist/hooks/useSyncOrchestrator.js +315 -860
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +11 -2
- package/dist/hooks/useTaskforce.js +290 -812
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +22 -10
- package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
- package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
- package/dist/hooks/workspace/workspaceLocalState.js +100 -0
- package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
- package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
- package/dist/localization/locales/en-US.js +2 -2
- package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
- package/dist/mcp/canonicalAssetHelpers.js +259 -0
- package/dist/mcp/clientIntegrations.d.ts +12 -1
- package/dist/mcp/clientIntegrations.js +161 -6
- package/dist/mcp/clientIntegrations.test.js +76 -4
- package/dist/mcp/documentHelpers.d.ts +100 -0
- package/dist/mcp/documentHelpers.js +161 -0
- package/dist/mcp/runtime.js +264 -505
- package/dist/mcp/runtime.test.js +353 -89
- package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
- package/dist/mcp/taskAttachmentHelpers.js +60 -0
- package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
- package/dist/mcp/taskAttachmentTypes.js +1 -0
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +15 -0
- package/dist/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +48 -3
- package/dist/server/index.test.js +14 -2
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +91 -40
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +231 -55
- package/dist/server/routes/billing.js +473 -39
- package/dist/server/routes/billing.test.js +972 -37
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +31 -3
- package/dist/server/routes/sync.integration.test.js +180 -0
- package/dist/server/routes/sync.js +125 -242
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +11 -22
- package/dist/server/routes.test.js +530 -81
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +1 -0
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.js +4 -0
- package/dist/sync/collaborationSyncPayload.test.js +8 -0
- package/dist/sync/collaborationSyncState.d.ts +3 -1
- package/dist/sync/collaborationSyncState.js +19 -0
- package/dist/sync/contentSyncState.d.ts +10 -0
- package/dist/sync/contentSyncState.js +277 -14
- package/dist/sync/syncApplyHandlers.d.ts +87 -1
- package/dist/sync/syncApplyHandlers.js +370 -8
- package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
- package/dist/sync/syncApplyHandlers.test.js +377 -0
- package/dist/sync/syncService.d.ts +9 -1
- package/dist/sync/syncService.js +57 -10
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +11 -0
- package/dist/sync/workspacePullFeed.js +123 -68
- package/dist/sync/workspacePullFeed.test.js +180 -0
- package/dist/sync/workspaceSyncModel.d.ts +22 -1
- package/dist/sync/workspaceSyncModel.js +65 -0
- package/dist/sync/workspaceSyncModel.test.js +119 -0
- package/dist/types.d.ts +2 -10
- package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
- package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
- package/dist/ui/assets/index-DXUdtH1B.js +6 -0
- package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
- package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +5 -3
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
- package/dist/ui/assets/index-MXeWoebC.css +0 -1
- package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
- package/dist/utils/billingStatusCache.d.ts +0 -21
- 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" })),
|
|
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'
|
|
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 {
|
|
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
|
-
|
|
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 [
|
|
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(
|
|
93
|
+
const entitlementState = String(accountProfileSummary?.entitlementState || '').trim().toLowerCase();
|
|
86
94
|
return ACTIVE_ENTITLEMENT_STATES.has(entitlementState);
|
|
87
|
-
}, [
|
|
95
|
+
}, [accountProfileSummary?.entitlementState]);
|
|
88
96
|
const gateMessage = useMemo(() => {
|
|
89
97
|
if (hasResolvedActiveEntitlement)
|
|
90
98
|
return null;
|
|
91
|
-
|
|
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 (
|
|
110
|
+
if (effectiveGateState === 'checkout_pending')
|
|
111
|
+
return GATE_MESSAGES.checkoutPending;
|
|
112
|
+
if (effectiveGateState === 'misconfigured')
|
|
94
113
|
return GATE_MESSAGES.misconfigured;
|
|
95
|
-
if (
|
|
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
|
|
120
|
-
const
|
|
138
|
+
const loadAccountProfileSummary = useCallback(async (options) => {
|
|
139
|
+
const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);
|
|
121
140
|
if (!isAuthenticated) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return;
|
|
141
|
+
setAccountProfileSummary(null);
|
|
142
|
+
invalidateAccountProfileSummaryCache(accountProfileSummaryUrl, { identityKey: authUserId });
|
|
143
|
+
return null;
|
|
125
144
|
}
|
|
126
145
|
try {
|
|
127
|
-
const payload = await
|
|
128
|
-
|
|
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
|
-
}, [
|
|
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(
|
|
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
|
-
|
|
157
|
-
await
|
|
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
|
-
|
|
163
|
-
await
|
|
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
|
-
|
|
169
|
-
await
|
|
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
|
-
}, [
|
|
256
|
+
}, [authUserId, buildCloudBillingUrl, hasResolvedActiveEntitlement, loadAccountProfileSummary, onContinueToTaskforce, runtimeMode]);
|
|
204
257
|
const confirmCheckoutSession = useCallback(async (sessionId) => {
|
|
205
|
-
if (!sessionId)
|
|
206
|
-
return
|
|
258
|
+
if (!sessionId) {
|
|
259
|
+
return {
|
|
260
|
+
confirmed: false,
|
|
261
|
+
errorMessage: 'Checkout session is missing.'
|
|
262
|
+
};
|
|
263
|
+
}
|
|
207
264
|
try {
|
|
208
|
-
const res = await fetch(
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
await loadBillingStatus();
|
|
221
|
-
return true;
|
|
284
|
+
return { confirmed: true };
|
|
222
285
|
}
|
|
223
286
|
catch {
|
|
224
|
-
|
|
225
|
-
|
|
287
|
+
return {
|
|
288
|
+
confirmed: false,
|
|
289
|
+
errorMessage: 'Unable to confirm checkout completion.'
|
|
290
|
+
};
|
|
226
291
|
}
|
|
227
|
-
}, [
|
|
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(
|
|
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
|
-
}, [
|
|
323
|
+
}, [buildCloudBillingUrl, isAuthenticated, navigate]);
|
|
259
324
|
useEffect(() => {
|
|
260
|
-
void
|
|
261
|
-
}, [
|
|
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
|
|
344
|
+
const confirmation = querySessionId
|
|
280
345
|
? await confirmCheckoutSession(querySessionId)
|
|
281
|
-
: false;
|
|
282
|
-
if (!confirmed) {
|
|
283
|
-
await
|
|
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
|
-
|
|
292
|
-
|
|
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:
|
|
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,
|
|
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 (!
|
|
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
|
|
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(
|
|
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 {};
|