@taskforcehq/taskforce 0.3.303 → 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 +102 -5
- package/dist/TaskforceCore.js +514 -212
- package/dist/TaskforceCore.test.js +1172 -133
- 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/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 +5 -1
- package/dist/components/views/PlansPage.js +39 -12
- package/dist/components/views/PlansPage.test.js +59 -2
- package/dist/components/views/StandaloneLayout.js +113 -38
- package/dist/components/views/WidgetView.js +11 -2
- package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
- package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
- package/dist/core/PlanEntitlementService.d.ts +3 -1
- package/dist/core/PlanEntitlementService.js +61 -11
- package/dist/core/SignupMonetizationSettings.test.js +37 -2
- 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 +15 -11
- package/dist/core/Taskforce.js +43 -2
- package/dist/core/WorkspacePlanMode.test.js +65 -0
- 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 +314 -873
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
- package/dist/hooks/useTaskMutations.js +45 -6
- package/dist/hooks/useTaskMutations.test.js +147 -0
- package/dist/hooks/useTaskforce.d.ts +8 -2
- package/dist/hooks/useTaskforce.js +285 -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 +174 -476
- package/dist/mcp/runtime.test.js +81 -8
- 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/plugins/vite.js +1 -1
- package/dist/runtime/appGateDefinitions.d.ts +1 -1
- package/dist/runtime/appGateDefinitions.js +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +46 -3
- package/dist/server/index.test.js +12 -1
- package/dist/server/routes/admin.js +13 -19
- package/dist/server/routes/auth.js +37 -2
- package/dist/server/routes/billing.js +18 -67
- package/dist/server/routes/billing.test.js +43 -0
- package/dist/server/routes/shared.d.ts +30 -0
- package/dist/server/routes/shared.js +26 -0
- package/dist/server/routes/sync.integration.test.js +20 -0
- package/dist/server/routes/sync.js +110 -237
- package/dist/server/routes/tasks.js +49 -7
- package/dist/server/routes.js +8 -19
- package/dist/server/routes.test.js +243 -5
- package/dist/shared/taskActor.d.ts +10 -0
- package/dist/shared/taskActor.js +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.js +12 -10
- 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 +8 -1
- package/dist/sync/syncService.js +36 -2
- package/dist/sync/syncService.test.js +48 -0
- package/dist/sync/workspacePullFeed.d.ts +1 -0
- package/dist/sync/workspacePullFeed.js +66 -59
- package/dist/sync/workspacePullFeed.test.js +72 -4
- 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-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
- package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.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-CGk9s3NJ.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-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
- package/dist/ui/index.html +4 -4
- package/dist/utils/taskActivity.js +4 -0
- package/dist/utils/taskActivity.test.js +25 -1
- package/package.json +3 -1
- package/scripts/playwright-auth.sh +4 -0
- package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
- package/dist/ui/assets/index-CLIMgR6g.js +0 -6
- package/dist/ui/assets/index-DneETxcu.css +0 -1
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { type AccountProfileSummary } from '../../utils/accountProfileSummaryCache';
|
|
1
2
|
interface PlansPageProps {
|
|
2
3
|
isAuthenticated: boolean;
|
|
3
4
|
authUserId?: string | null;
|
|
@@ -11,6 +12,9 @@ interface PlansPageProps {
|
|
|
11
12
|
resolveCloudAuthUrl?: (path: string) => string;
|
|
12
13
|
embedded?: boolean;
|
|
13
14
|
shellOwnsScroll?: boolean;
|
|
15
|
+
onAccountProfileSummaryChange?: (summary: AccountProfileSummary | null) => void;
|
|
16
|
+
onContinueToTaskforce?: () => void | Promise<void>;
|
|
17
|
+
continueBusy?: boolean;
|
|
14
18
|
}
|
|
15
|
-
export declare function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessionResolved, currentTheme, projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, resolveCloudAuthUrl, 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;
|
|
16
20
|
export {};
|
|
@@ -38,7 +38,7 @@ function buildEmbeddedPlansPath(params = {}) {
|
|
|
38
38
|
}
|
|
39
39
|
return `/?${query.toString()}`;
|
|
40
40
|
}
|
|
41
|
-
export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessionResolved, currentTheme = 'dark', projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, resolveCloudAuthUrl, 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 }) {
|
|
42
42
|
const navigate = useNavigate();
|
|
43
43
|
const location = useLocation();
|
|
44
44
|
const [accountProfileSummary, setAccountProfileSummary] = useState(null);
|
|
@@ -148,13 +148,14 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
148
148
|
force: options?.force === true
|
|
149
149
|
});
|
|
150
150
|
setAccountProfileSummary(payload);
|
|
151
|
+
onAccountProfileSummaryChange?.(payload);
|
|
151
152
|
return payload;
|
|
152
153
|
}
|
|
153
154
|
catch {
|
|
154
155
|
// best effort only
|
|
155
156
|
return null;
|
|
156
157
|
}
|
|
157
|
-
}, [authUserId, buildCloudBillingUrl, isAuthenticated]);
|
|
158
|
+
}, [authUserId, buildCloudBillingUrl, isAuthenticated, onAccountProfileSummaryChange]);
|
|
158
159
|
const resolveRecoveryMessage = useCallback((summary, fallback) => {
|
|
159
160
|
const billingConflictCode = String(summary?.billingConflictCode || '').trim().toUpperCase();
|
|
160
161
|
if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
|
|
@@ -166,11 +167,20 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
166
167
|
return summaryMessage;
|
|
167
168
|
return String(fallback || 'Unable to confirm checkout completion.').trim() || 'Unable to confirm checkout completion.';
|
|
168
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
|
+
}, []);
|
|
169
178
|
const startCheckout = useCallback(async (planVersionId, interval) => {
|
|
170
179
|
if (!planVersionId) {
|
|
171
180
|
setFeedback({ type: 'error', message: 'Selected plan is missing a default plan version.' });
|
|
172
181
|
return;
|
|
173
182
|
}
|
|
183
|
+
const hadActiveEntitlement = hasResolvedActiveEntitlement;
|
|
174
184
|
setActionBusy(true);
|
|
175
185
|
setFeedback(null);
|
|
176
186
|
try {
|
|
@@ -189,13 +199,21 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
189
199
|
if (checkoutState === 'free') {
|
|
190
200
|
setFeedback({ type: 'success', message: 'Plan selected. Your access is ready.' });
|
|
191
201
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
192
|
-
await loadAccountProfileSummary();
|
|
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
|
+
}
|
|
193
207
|
return;
|
|
194
208
|
}
|
|
195
209
|
if (checkoutState === 'updated') {
|
|
196
210
|
setFeedback({ type: 'success', message: String(payload?.message || 'Your subscription was updated successfully.') });
|
|
197
211
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
198
|
-
await loadAccountProfileSummary();
|
|
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
|
+
}
|
|
199
217
|
return;
|
|
200
218
|
}
|
|
201
219
|
if (checkoutState === 'already_current_plan') {
|
|
@@ -235,7 +253,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
235
253
|
finally {
|
|
236
254
|
setActionBusy(false);
|
|
237
255
|
}
|
|
238
|
-
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, runtimeMode]);
|
|
256
|
+
}, [authUserId, buildCloudBillingUrl, hasResolvedActiveEntitlement, loadAccountProfileSummary, onContinueToTaskforce, runtimeMode]);
|
|
239
257
|
const confirmCheckoutSession = useCallback(async (sessionId) => {
|
|
240
258
|
if (!sessionId) {
|
|
241
259
|
return {
|
|
@@ -258,7 +276,11 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
258
276
|
};
|
|
259
277
|
}
|
|
260
278
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
261
|
-
await loadAccountProfileSummary();
|
|
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?.();
|
|
283
|
+
}
|
|
262
284
|
return { confirmed: true };
|
|
263
285
|
}
|
|
264
286
|
catch {
|
|
@@ -267,7 +289,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
267
289
|
errorMessage: 'Unable to confirm checkout completion.'
|
|
268
290
|
};
|
|
269
291
|
}
|
|
270
|
-
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary]);
|
|
292
|
+
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, onContinueToTaskforce]);
|
|
271
293
|
const openBillingPortal = useCallback(async () => {
|
|
272
294
|
if (!isAuthenticated) {
|
|
273
295
|
navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`);
|
|
@@ -353,7 +375,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
353
375
|
return;
|
|
354
376
|
}
|
|
355
377
|
if (gateMessage) {
|
|
356
|
-
setFeedback({ type:
|
|
378
|
+
setFeedback({ type: resolveGateFeedbackType(gateMessage), message: gateMessage });
|
|
357
379
|
return;
|
|
358
380
|
}
|
|
359
381
|
setFeedback((current) => {
|
|
@@ -371,7 +393,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
371
393
|
return null;
|
|
372
394
|
return current;
|
|
373
395
|
});
|
|
374
|
-
}, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveRecoveryMessage, scheduleTransientQueryClear, startCheckout]);
|
|
396
|
+
}, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveGateFeedbackType, resolveRecoveryMessage, scheduleTransientQueryClear, startCheckout]);
|
|
375
397
|
const handleSelectPlan = useCallback(async (intent) => {
|
|
376
398
|
if (!isAuthenticated) {
|
|
377
399
|
const nextCheckoutState = intent.pricingType === 'free' ? 'free' : 'start';
|
|
@@ -399,6 +421,10 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
399
421
|
await startCheckout(intent.planVersionId, intent.interval);
|
|
400
422
|
}, [isAuthenticated, navigate, openBillingPortal, startCheckout]);
|
|
401
423
|
const handleBack = useCallback(() => {
|
|
424
|
+
if (hasResolvedActiveEntitlement && onContinueToTaskforce) {
|
|
425
|
+
void onContinueToTaskforce();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
402
428
|
if (embedded) {
|
|
403
429
|
closeEmbeddedPlansScreen();
|
|
404
430
|
return;
|
|
@@ -408,8 +434,9 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
408
434
|
return;
|
|
409
435
|
}
|
|
410
436
|
navigate('/');
|
|
411
|
-
}, [closeEmbeddedPlansScreen, embedded, navigate]);
|
|
412
|
-
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"] }))] }));
|
|
413
440
|
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, actions: topActions }));
|
|
414
|
-
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: 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 }));
|
|
415
442
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
5
|
import { PlansPage } from './PlansPage';
|
|
6
6
|
vi.mock('./PlanComparisonPage', () => ({
|
|
7
|
-
PlanComparisonPage: ({ heading, statusBanner }) => (_jsxs("div", { children: [_jsx("h1", { children: heading }), statusBanner?.message ? _jsx("div", { children: statusBanner.message }) : null
|
|
7
|
+
PlanComparisonPage: ({ heading, statusBanner, topActions, onSelectPlan, onBack, backLabel, backDisabled }) => (_jsxs("div", { children: [_jsx("h1", { children: heading }), statusBanner?.message ? _jsx("div", { "data-status-type": statusBanner.type, children: statusBanner.message }) : null, topActions, onSelectPlan ? (_jsx("button", { onClick: () => onSelectPlan({
|
|
8
|
+
planId: 'free',
|
|
9
|
+
planVersionId: 'free-v1',
|
|
10
|
+
interval: 'month',
|
|
11
|
+
pricingType: 'free',
|
|
12
|
+
isCurrentPlan: false
|
|
13
|
+
}), children: "Select Free Plan" })) : null, onBack ? (_jsx("button", { onClick: onBack, disabled: backDisabled, children: backLabel || 'Back' })) : null] }))
|
|
8
14
|
}));
|
|
9
15
|
describe('PlansPage', () => {
|
|
10
16
|
afterEach(() => {
|
|
@@ -97,4 +103,55 @@ describe('PlansPage', () => {
|
|
|
97
103
|
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&checkout=cancel'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u3", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path }) }));
|
|
98
104
|
expect(await screen.findByText('Your previous checkout expired. Choose a plan to continue.')).toBeInTheDocument();
|
|
99
105
|
});
|
|
106
|
+
it('auto-continues after a free plan grant and exposes a taskforce CTA once access is active', async () => {
|
|
107
|
+
const onContinueToTaskforce = vi.fn();
|
|
108
|
+
const fetchMock = vi.fn()
|
|
109
|
+
.mockResolvedValueOnce({
|
|
110
|
+
ok: true,
|
|
111
|
+
json: async () => ({
|
|
112
|
+
entitlementState: 'pending_plan_selection',
|
|
113
|
+
gate: 'plan_selection_required',
|
|
114
|
+
message: 'Choose a plan to continue.'
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
.mockResolvedValueOnce({
|
|
118
|
+
ok: true,
|
|
119
|
+
json: async () => ({
|
|
120
|
+
checkoutState: 'free'
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
.mockResolvedValueOnce({
|
|
124
|
+
ok: true,
|
|
125
|
+
json: async () => ({
|
|
126
|
+
entitlementState: 'active',
|
|
127
|
+
planId: 'free',
|
|
128
|
+
planVersionId: 'free-v1',
|
|
129
|
+
gate: 'ok',
|
|
130
|
+
message: null
|
|
131
|
+
})
|
|
132
|
+
});
|
|
133
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
134
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=plan_selection_required'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u4", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path, onContinueToTaskforce: onContinueToTaskforce }) }));
|
|
135
|
+
expect(await screen.findByText('Choose a plan to continue.')).toBeInTheDocument();
|
|
136
|
+
fireEvent.click(screen.getByRole('button', { name: 'Select Free Plan' }));
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(onContinueToTaskforce).toHaveBeenCalledTimes(1);
|
|
139
|
+
});
|
|
140
|
+
expect(screen.getByRole('button', { name: 'Take Me to Taskforce' })).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
it('disables the back action while the account is still entitlement-blocked', async () => {
|
|
143
|
+
const fetchMock = vi.fn()
|
|
144
|
+
.mockResolvedValueOnce({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: async () => ({
|
|
147
|
+
entitlementState: 'pending_plan_selection',
|
|
148
|
+
gate: 'plan_selection_required',
|
|
149
|
+
message: 'Choose a plan to continue.'
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
153
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&gate=plan_selection_required'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u5", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path }) }));
|
|
154
|
+
expect(await screen.findByRole('button', { name: 'Back to App' })).toBeDisabled();
|
|
155
|
+
expect(screen.getByText('Choose a plan to continue.')).toHaveAttribute('data-status-type', 'info');
|
|
156
|
+
});
|
|
100
157
|
});
|