@taskforcehq/taskforce 0.3.305 → 0.3.306
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 +295 -89
- package/dist/TaskforceCore.js +215 -43
- package/dist/TaskforceCore.test.js +433 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +549 -0
- package/dist/components/features/AgentsModule.d.ts +10 -1
- package/dist/components/features/AgentsModule.js +260 -15
- package/dist/components/features/AgentsModule.test.js +255 -5
- package/dist/components/features/DocumentViewer.d.ts +5 -1
- package/dist/components/features/DocumentViewer.js +60 -3
- package/dist/components/features/DocumentWorkspace.js +1 -1
- package/dist/components/features/DocumentWorkspace.test.js +33 -0
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskCard.js +19 -28
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskForm.js +85 -90
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -13
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +70 -14
- package/dist/components/views/PlanComparisonPage.test.js +238 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +283 -533
- package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
- package/dist/components/views/panels/PlanningDrawer.js +18 -11
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/core/AiProfileService.d.ts +39 -2
- package/dist/core/AiProfileService.js +639 -73
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +566 -7
- package/dist/core/AttachmentLinkService.js +57 -15
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +115 -6
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +52 -0
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/Taskforce.d.ts +225 -14
- package/dist/core/Taskforce.js +765 -192
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +68 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
- package/dist/hooks/sync/orchestratorShared.js +68 -47
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +171 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +15 -3
- package/dist/hooks/sync/transfers.js +179 -62
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +396 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +359 -178
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +60 -2
- package/dist/hooks/useTaskforce.js +549 -240
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +6 -1
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/documentAssetRegistrar.js +3 -3
- package/dist/mcp/runtime.d.ts +2 -0
- package/dist/mcp/runtime.js +131 -64
- package/dist/mcp/runtime.test.js +291 -12
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +171 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +120 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +106 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +272 -19
- package/dist/server/routes/auth.d.ts +15 -2
- package/dist/server/routes/auth.js +994 -57
- package/dist/server/routes/authSupport.d.ts +1 -1
- package/dist/server/routes/authSupport.js +1 -1
- package/dist/server/routes/billing.js +361 -35
- package/dist/server/routes/billing.test.js +592 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +172 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -4
- package/dist/server/routes/sync.integration.test.js +437 -39
- package/dist/server/routes/sync.js +61 -605
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +180 -0
- package/dist/server/routes/tasks.js +43 -1
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +60 -3
- package/dist/server/routes.test.js +1824 -171
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +17 -11
- package/dist/sync/collaborationSyncPayload.test.js +13 -1
- package/dist/sync/contentSyncState.d.ts +4 -8
- package/dist/sync/contentSyncState.js +17 -70
- package/dist/sync/syncApplyHandlers.d.ts +7 -0
- package/dist/sync/syncApplyHandlers.js +28 -13
- package/dist/sync/syncApplyHandlers.test.js +70 -3
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +44 -0
- package/dist/sync/syncResourceAdapters.js +77 -0
- package/dist/sync/syncService.d.ts +5 -0
- package/dist/sync/syncService.js +52 -0
- package/dist/sync/syncService.test.js +57 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.js +4 -3
- package/dist/sync/workspacePullFeed.test.js +43 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +45 -0
- package/dist/sync/workspaceSyncModel.js +183 -28
- package/dist/sync/workspaceSyncModel.test.js +291 -12
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +17 -0
- package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
- package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
- package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
- package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
- package/dist/ui/assets/index-C6k75jr8.js +6 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/assignees.d.ts +2 -0
- package/dist/utils/assignees.js +2 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +2 -0
- package/dist/utils/taskActivity.js +228 -22
- package/dist/utils/taskActivity.test.js +119 -8
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface EditableAvatarButtonProps {
|
|
3
|
+
label: string;
|
|
4
|
+
imageUrl?: string | null;
|
|
5
|
+
fallback?: React.ReactNode;
|
|
6
|
+
accentColor?: string | null;
|
|
7
|
+
size?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
radius?: number | string;
|
|
11
|
+
editBadgeSize?: number | string;
|
|
12
|
+
editIconSize?: number;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
onClick: () => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function EditableAvatarButton({ label, imageUrl, fallback, accentColor, size, width, height, radius, editBadgeSize, editIconSize, disabled, className, onClick, }: EditableAvatarButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pencil } from 'lucide-react';
|
|
3
|
+
import styles from './EditableAvatarButton.module.css';
|
|
4
|
+
export function EditableAvatarButton({ label, imageUrl, fallback, accentColor, size = 52, width, height, radius, editBadgeSize, editIconSize = 16, disabled = false, className = '', onClick, }) {
|
|
5
|
+
const normalizedImageUrl = String(imageUrl || '').trim();
|
|
6
|
+
const resolvedWidth = width ?? size;
|
|
7
|
+
const resolvedHeight = height ?? size;
|
|
8
|
+
const resolvedRadius = typeof radius === 'number' ? `${radius}px` : radius;
|
|
9
|
+
const resolvedEditBadgeSize = typeof editBadgeSize === 'number' ? `${editBadgeSize}px` : editBadgeSize;
|
|
10
|
+
return (_jsxs("button", { type: "button", className: `${styles.editableAvatarButton} ${className}`.trim(), "aria-label": label, title: label, onClick: onClick, disabled: disabled, style: {
|
|
11
|
+
['--editable-avatar-size']: `${size}px`,
|
|
12
|
+
['--editable-avatar-width']: `${resolvedWidth}px`,
|
|
13
|
+
['--editable-avatar-height']: `${resolvedHeight}px`,
|
|
14
|
+
...(resolvedRadius ? { ['--editable-avatar-radius']: resolvedRadius } : {}),
|
|
15
|
+
...(resolvedEditBadgeSize ? { ['--editable-avatar-badge-size']: resolvedEditBadgeSize } : {}),
|
|
16
|
+
...(accentColor ? { ['--editable-avatar-accent']: accentColor } : {})
|
|
17
|
+
}, children: [normalizedImageUrl ? (_jsx("img", { src: normalizedImageUrl, alt: "", className: styles.editableAvatarImage })) : (_jsx("span", { className: styles.editableAvatarFallback, children: fallback })), _jsx("span", { className: styles.editableAvatarBadge, "aria-hidden": "true", children: _jsx(Pencil, { size: editIconSize }) })] }));
|
|
18
|
+
}
|
|
@@ -4,6 +4,7 @@ export type BillingPricingType = 'stripe' | 'free';
|
|
|
4
4
|
export interface PublicPricingPlan {
|
|
5
5
|
planId: string;
|
|
6
6
|
displayName: string;
|
|
7
|
+
description?: string | null;
|
|
7
8
|
status: string;
|
|
8
9
|
defaultVersion: {
|
|
9
10
|
planVersionId: string;
|
|
@@ -15,6 +16,10 @@ export interface PublicPricingPlan {
|
|
|
15
16
|
featureKey: string;
|
|
16
17
|
access: string;
|
|
17
18
|
config?: Record<string, unknown>;
|
|
19
|
+
label?: string | null;
|
|
20
|
+
description?: string | null;
|
|
21
|
+
publicLabel?: string | null;
|
|
22
|
+
publicDescription?: string | null;
|
|
18
23
|
}>;
|
|
19
24
|
pricing: {
|
|
20
25
|
month: {
|
|
@@ -55,6 +60,7 @@ interface PlanComparisonPageProps {
|
|
|
55
60
|
currentPlanId?: string | null;
|
|
56
61
|
currentPlanVersionId?: string | null;
|
|
57
62
|
currentEntitlementState?: string | null;
|
|
63
|
+
markCurrentPlan?: boolean;
|
|
58
64
|
canManageCurrentPlan?: boolean;
|
|
59
65
|
pricingEndpoint?: string;
|
|
60
66
|
statusBanner?: {
|
|
@@ -67,8 +73,10 @@ interface PlanComparisonPageProps {
|
|
|
67
73
|
topActions?: React.ReactNode;
|
|
68
74
|
shellOwnsScroll?: boolean;
|
|
69
75
|
showPageBackButton?: boolean;
|
|
76
|
+
showHeaderBarWithChrome?: boolean;
|
|
77
|
+
currentPlanCardActionMode?: 'manage' | 'indicator' | 'hidden';
|
|
70
78
|
fallbackPricingSource?: PricingSourceDetails | null;
|
|
71
79
|
onSelectPlan: (intent: PlanSelectionIntent) => void | Promise<void>;
|
|
72
80
|
}
|
|
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;
|
|
81
|
+
export declare function PlanComparisonPage({ heading, subtitle, variant, chrome, footer, isAuthenticated, authSessionResolved, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled, topActions, shellOwnsScroll, showPageBackButton, showHeaderBarWithChrome, currentPlanCardActionMode, fallbackPricingSource, onSelectPlan }: PlanComparisonPageProps): import("react/jsx-runtime").JSX.Element;
|
|
74
82
|
export {};
|
|
@@ -61,7 +61,49 @@ function getPlanSortRank(plan) {
|
|
|
61
61
|
return Math.min(...numericAmounts);
|
|
62
62
|
return Number.POSITIVE_INFINITY;
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
function formatFeatureKeyFallback(featureKey) {
|
|
65
|
+
return String(featureKey || '')
|
|
66
|
+
.replace(/[_\-.]+/g, ' ')
|
|
67
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
68
|
+
}
|
|
69
|
+
function readConfiguredFeatureLimit(config, key) {
|
|
70
|
+
const rawValue = config?.[key];
|
|
71
|
+
const numericValue = Number(rawValue);
|
|
72
|
+
if (!Number.isFinite(numericValue))
|
|
73
|
+
return null;
|
|
74
|
+
const normalized = Math.max(1, Math.floor(numericValue));
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
function getFeatureDisplayParts(feature, seatLimit) {
|
|
78
|
+
const baseLabel = String(feature.publicLabel || feature.label || '').trim() || formatFeatureKeyFallback(feature.featureKey);
|
|
79
|
+
const config = feature.config && typeof feature.config === 'object' ? feature.config : {};
|
|
80
|
+
if (feature.featureKey === 'context.uploads') {
|
|
81
|
+
const storageLimitMb = readConfiguredFeatureLimit(config, 'storageLimitMb');
|
|
82
|
+
if (storageLimitMb !== null) {
|
|
83
|
+
return { limitValue: String(storageLimitMb), limitUnit: 'MB', baseLabel };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (feature.featureKey === 'workspace.workspaces') {
|
|
87
|
+
const maxWorkspaces = readConfiguredFeatureLimit(config, 'maxWorkspaces');
|
|
88
|
+
if (maxWorkspaces !== null) {
|
|
89
|
+
return { limitValue: String(maxWorkspaces), limitUnit: 'x', baseLabel };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (feature.featureKey === 'collaboration.ai_profiles') {
|
|
93
|
+
const maxAiProfiles = readConfiguredFeatureLimit(config, 'maxAiProfiles');
|
|
94
|
+
if (maxAiProfiles !== null) {
|
|
95
|
+
return { limitValue: String(maxAiProfiles), limitUnit: 'x', baseLabel };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (feature.featureKey === 'collaboration.team_management') {
|
|
99
|
+
const normalizedSeatLimit = Number(seatLimit);
|
|
100
|
+
if (Number.isFinite(normalizedSeatLimit) && normalizedSeatLimit > 0) {
|
|
101
|
+
return { limitValue: String(Math.floor(normalizedSeatLimit)), limitUnit: 'x', baseLabel };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { limitValue: null, limitUnit: null, baseLabel };
|
|
105
|
+
}
|
|
106
|
+
export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, markCurrentPlan, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, showHeaderBarWithChrome = false, currentPlanCardActionMode = 'manage', fallbackPricingSource = null, onSelectPlan }) {
|
|
65
107
|
const [plans, setPlans] = useState([]);
|
|
66
108
|
const [pricingSource, setPricingSource] = useState(null);
|
|
67
109
|
const [loading, setLoading] = useState(true);
|
|
@@ -114,6 +156,7 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
114
156
|
return String(a.displayName || a.planId).localeCompare(String(b.displayName || b.planId));
|
|
115
157
|
}), [plans]);
|
|
116
158
|
const pricingSourceLabel = useMemo(() => formatPricingSource(pricingSource || fallbackPricingSource), [fallbackPricingSource, pricingSource]);
|
|
159
|
+
const planSelectionEnabled = authSessionResolved;
|
|
117
160
|
const contentClassName = `${styles.plansWrapper} ${shellOwnsScroll ? styles.shellOwnsScroll : 'tf-scrollbar'} ${variant === 'site' ? styles.siteVariant : ''} ${chrome ? styles.withChrome : ''}`.trim();
|
|
118
161
|
const wrapPage = (content) => {
|
|
119
162
|
if (shellOwnsScroll) {
|
|
@@ -121,13 +164,13 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
121
164
|
}
|
|
122
165
|
return (_jsxs("div", { className: taskforceStyles.standaloneWrapper, "data-theme": "dark", children: [chrome, content, footer] }));
|
|
123
166
|
};
|
|
124
|
-
if (loading
|
|
167
|
+
if (loading) {
|
|
125
168
|
return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsx("div", { className: styles.contentFrame, children: _jsxs("div", { className: styles.loader, children: [_jsx(Loader2, { size: 48, className: styles.spinner }), _jsx("p", { children: "Loading plans..." })] }) }) }) }));
|
|
126
169
|
}
|
|
127
170
|
if (error) {
|
|
128
171
|
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" })] }) }) }) }));
|
|
129
172
|
}
|
|
130
|
-
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'
|
|
173
|
+
return wrapPage(_jsx(_Fragment, { children: _jsx("div", { className: contentClassName, children: _jsxs("div", { className: styles.contentFrame, children: [_jsxs("div", { className: styles.header, children: [(!chrome || showHeaderBarWithChrome) && (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'
|
|
131
174
|
? styles.successBanner
|
|
132
175
|
: (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) => {
|
|
133
176
|
const monthPrice = plan.pricing.month;
|
|
@@ -136,17 +179,27 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
136
179
|
const hasYear = isSelectablePrice(yearPrice);
|
|
137
180
|
const planVersionId = String(plan.defaultVersion?.planVersionId || '').trim();
|
|
138
181
|
const hasActiveEntitlement = isCommercialEntitlementActive(currentEntitlementState);
|
|
139
|
-
const
|
|
182
|
+
const shouldMarkCurrentPlan = markCurrentPlan ?? hasActiveEntitlement;
|
|
183
|
+
const hasKnownLinkedPlan = Boolean(planVersionId
|
|
140
184
|
&& currentPlanVersionId
|
|
141
|
-
&& currentPlanVersionId === planVersionId) || (!currentPlanVersionId
|
|
142
|
-
&& Boolean(currentPlanId && currentPlanId === plan.planId))
|
|
185
|
+
&& currentPlanVersionId === planVersionId) || Boolean(!currentPlanVersionId
|
|
186
|
+
&& Boolean(currentPlanId && currentPlanId === plan.planId));
|
|
187
|
+
const isCurrent = shouldMarkCurrentPlan && hasKnownLinkedPlan && currentEntitlementState !== 'canceled';
|
|
143
188
|
const defaultInterval = hasMonth ? 'month' : (hasYear ? 'year' : null);
|
|
144
189
|
const defaultPrice = defaultInterval === 'year' ? yearPrice : monthPrice;
|
|
145
|
-
const
|
|
190
|
+
const currentPlanShowsManageAction = currentPlanCardActionMode === 'manage';
|
|
191
|
+
const currentPlanShowsIndicator = currentPlanCardActionMode === 'indicator';
|
|
192
|
+
const canSelectCard = planSelectionEnabled
|
|
193
|
+
&& Boolean(planVersionId)
|
|
194
|
+
&& (isCurrent
|
|
195
|
+
? (currentPlanShowsManageAction && canManageCurrentPlan)
|
|
196
|
+
: Boolean(defaultInterval));
|
|
146
197
|
const handleCardSelect = () => {
|
|
147
198
|
if (!planVersionId)
|
|
148
199
|
return;
|
|
149
200
|
if (isCurrent) {
|
|
201
|
+
if (!currentPlanShowsManageAction)
|
|
202
|
+
return;
|
|
150
203
|
if (!canManageCurrentPlan)
|
|
151
204
|
return;
|
|
152
205
|
void onSelectPlan({
|
|
@@ -173,23 +226,26 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
173
226
|
event.preventDefault();
|
|
174
227
|
handleCardSelect();
|
|
175
228
|
}
|
|
176
|
-
} : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }),
|
|
229
|
+
} : undefined, children: [isCurrent && _jsx("span", { className: styles.currentBadge, children: "Current Plan" }), _jsx("h3", { className: styles.planName, children: plan.displayName }), String(plan.description || '').trim() && (_jsx("p", { className: styles.planDescription, children: String(plan.description || '').trim() })), _jsx("div", { className: styles.featuresList, children: Array.isArray(plan.features) && plan.features.length > 0 ? plan.features.map((feature, idx) => (_jsxs("div", { className: styles.featureItem, children: [_jsx(CheckCircle2, { className: styles.featureIcon }), _jsxs("div", { className: styles.featureTextBlock, children: [(() => {
|
|
230
|
+
const { limitValue, limitUnit, baseLabel } = getFeatureDisplayParts(feature, plan.defaultVersion?.seatLimit);
|
|
231
|
+
return (_jsxs("span", { className: styles.featureText, children: [limitValue && (_jsxs("span", { className: styles.featureLimitAccent, children: [_jsx("span", { children: limitValue }), limitUnit ? (_jsx("span", { className: limitUnit === 'MB' ? styles.featureLimitUnit : undefined, children: limitUnit })) : null] })), _jsx("span", { children: limitValue ? ` ${baseLabel}` : baseLabel })] }));
|
|
232
|
+
})(), String(feature.publicDescription || feature.description || '').trim() && (_jsx("span", { className: styles.featureDescription, children: String(feature.publicDescription || feature.description || '').trim() }))] })] }, `${feature.featureKey}-${idx}`))) : (_jsx("span", { className: styles.featureText, children: "No additional features included" })) }), (hasMonth || hasYear) ? (_jsxs("div", { className: styles.priceRows, children: [hasMonth ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(monthPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ month" })] })) : null, hasYear ? (_jsxs("div", { className: styles.priceRow, children: [_jsx("span", { className: styles.priceRowAmount, children: renderPriceLabel(yearPrice) }), _jsx("span", { className: styles.priceRowInterval, children: "/ year" })] })) : null] })) : null, isCurrent ? (currentPlanShowsManageAction ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-manage-${plan.planId}`, onClick: (event) => {
|
|
177
233
|
event.stopPropagation();
|
|
178
234
|
if (!canManageCurrentPlan)
|
|
179
235
|
return;
|
|
180
236
|
void onSelectPlan({ planId: plan.planId, planVersionId, interval: defaultInterval || 'month', pricingType: defaultPrice?.pricingType || 'stripe', isCurrentPlan: true });
|
|
181
|
-
}, disabled: !planVersionId || !canManageCurrentPlan, children: isAuthenticated
|
|
237
|
+
}, disabled: !planVersionId || !canManageCurrentPlan || !planSelectionEnabled, children: isAuthenticated
|
|
182
238
|
? (canManageCurrentPlan ? 'Manage Plan' : 'Current Plan')
|
|
183
|
-
: 'Current Plan' })) : (_jsxs("div", { className: styles.ctaRow, children: [_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
|
|
239
|
+
: 'Current Plan' })) : (currentPlanShowsIndicator ? (_jsx("button", { className: `${styles.callToAction} ${styles.currentCta}`, "data-testid": `plan-current-${plan.planId}`, disabled: true, children: "Current Plan" })) : null)) : (_jsxs("div", { className: styles.ctaRow, children: [hasMonth ? (_jsx("button", { className: `${styles.callToAction} ${styles.secondaryCta}`, "data-testid": `plan-select-${plan.planId}-month`, onClick: (event) => {
|
|
184
240
|
event.stopPropagation();
|
|
185
241
|
void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'month', pricingType: monthPrice?.pricingType || 'stripe', isCurrentPlan: false });
|
|
186
|
-
}, disabled: !planVersionId || !
|
|
242
|
+
}, disabled: !planVersionId || !planSelectionEnabled, children: monthPrice?.pricingType === 'free'
|
|
187
243
|
? (isAuthenticated ? 'Select Free' : 'Get Started Free')
|
|
188
|
-
: (isAuthenticated ? 'Choose Monthly' : 'Get Started Monthly') }), _jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
|
|
244
|
+
: (isAuthenticated ? 'Choose Monthly' : 'Get Started Monthly') })) : null, hasYear ? (_jsx("button", { className: `${styles.callToAction} ${styles.primaryCta}`, "data-testid": `plan-select-${plan.planId}-year`, onClick: (event) => {
|
|
189
245
|
event.stopPropagation();
|
|
190
246
|
void onSelectPlan({ planId: plan.planId, planVersionId, interval: 'year', pricingType: yearPrice?.pricingType || 'stripe', isCurrentPlan: false });
|
|
191
|
-
}, disabled: !planVersionId || !
|
|
247
|
+
}, disabled: !planVersionId || !planSelectionEnabled, children: yearPrice?.pricingType === 'free'
|
|
192
248
|
? (isAuthenticated ? 'Select Free' : 'Get Started Free')
|
|
193
|
-
: (isAuthenticated ? 'Choose Yearly' : 'Get Started Yearly') })] }))] }, plan.planId));
|
|
249
|
+
: (isAuthenticated ? 'Choose Yearly' : 'Get Started Yearly') })) : null] }))] }, plan.planId));
|
|
194
250
|
}) }))] }) }) }));
|
|
195
251
|
}
|
|
@@ -17,6 +17,7 @@ describe('PlanComparisonPage', () => {
|
|
|
17
17
|
{
|
|
18
18
|
planId: 'free',
|
|
19
19
|
displayName: 'Free',
|
|
20
|
+
description: 'Start using Taskforce at no cost.',
|
|
20
21
|
status: 'active',
|
|
21
22
|
defaultVersion: {
|
|
22
23
|
planVersionId: 'free-v1',
|
|
@@ -33,6 +34,7 @@ describe('PlanComparisonPage', () => {
|
|
|
33
34
|
{
|
|
34
35
|
planId: 'team-5',
|
|
35
36
|
displayName: 'Team 5',
|
|
37
|
+
description: 'For small collaborative teams.',
|
|
36
38
|
status: 'active',
|
|
37
39
|
defaultVersion: {
|
|
38
40
|
planVersionId: 'team-5-v2',
|
|
@@ -59,6 +61,11 @@ describe('PlanComparisonPage', () => {
|
|
|
59
61
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
60
62
|
expect(await screen.findByText('Pricing source: production (cloud)')).toBeInTheDocument();
|
|
61
63
|
});
|
|
64
|
+
it('renders plan descriptions when provided by the pricing payload', async () => {
|
|
65
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
66
|
+
expect(await screen.findByText('Start using Taskforce at no cost.')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText('For small collaborative teams.')).toBeInTheDocument();
|
|
68
|
+
});
|
|
62
69
|
it('falls back to provided pricing source details when the pricing response omits them', async () => {
|
|
63
70
|
globalThis.__DEBUG_MODE__ = true;
|
|
64
71
|
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
@@ -92,6 +99,188 @@ describe('PlanComparisonPage', () => {
|
|
|
92
99
|
await screen.findByRole('heading', { name: 'Subscription Plans' });
|
|
93
100
|
expect(screen.queryByText('Pricing source: production (cloud)')).not.toBeInTheDocument();
|
|
94
101
|
});
|
|
102
|
+
it('renders public feature copy when provided by the pricing payload', async () => {
|
|
103
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
104
|
+
ok: true,
|
|
105
|
+
json: async () => ({
|
|
106
|
+
pricingSource: {
|
|
107
|
+
environment: 'production',
|
|
108
|
+
runtimeMode: 'cloud'
|
|
109
|
+
},
|
|
110
|
+
plans: [
|
|
111
|
+
{
|
|
112
|
+
planId: 'team-5',
|
|
113
|
+
displayName: 'Team 5',
|
|
114
|
+
status: 'active',
|
|
115
|
+
defaultVersion: {
|
|
116
|
+
planVersionId: 'team-5-v2',
|
|
117
|
+
versionNumber: 2,
|
|
118
|
+
seatLimit: 5,
|
|
119
|
+
metadata: {}
|
|
120
|
+
},
|
|
121
|
+
features: [
|
|
122
|
+
{
|
|
123
|
+
featureKey: 'collaboration.ai_profiles',
|
|
124
|
+
access: 'enabled',
|
|
125
|
+
label: 'AI Profiles',
|
|
126
|
+
publicLabel: 'AI teammates',
|
|
127
|
+
publicDescription: 'Add reusable AI teammates to your workspace.'
|
|
128
|
+
}
|
|
129
|
+
],
|
|
130
|
+
pricing: {
|
|
131
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 2000, currency: 'usd', interval: 'month' },
|
|
132
|
+
year: null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
})));
|
|
138
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
139
|
+
expect(await screen.findByText('AI teammates')).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByText('Add reusable AI teammates to your workspace.')).toBeInTheDocument();
|
|
141
|
+
expect(screen.queryByText('Collaboration Ai Profiles')).not.toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
it('renders inline numeric feature limits next to each feature label', async () => {
|
|
144
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
145
|
+
ok: true,
|
|
146
|
+
json: async () => ({
|
|
147
|
+
pricingSource: {
|
|
148
|
+
environment: 'production',
|
|
149
|
+
runtimeMode: 'cloud'
|
|
150
|
+
},
|
|
151
|
+
plans: [
|
|
152
|
+
{
|
|
153
|
+
planId: 'free',
|
|
154
|
+
displayName: 'Free',
|
|
155
|
+
status: 'active',
|
|
156
|
+
defaultVersion: {
|
|
157
|
+
planVersionId: 'free-v1',
|
|
158
|
+
versionNumber: 1,
|
|
159
|
+
seatLimit: 12,
|
|
160
|
+
metadata: {}
|
|
161
|
+
},
|
|
162
|
+
features: [
|
|
163
|
+
{
|
|
164
|
+
featureKey: 'workspace.workspaces',
|
|
165
|
+
access: 'enabled',
|
|
166
|
+
label: 'Workspaces',
|
|
167
|
+
config: { maxWorkspaces: 1 }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
featureKey: 'collaboration.ai_profiles',
|
|
171
|
+
access: 'limited',
|
|
172
|
+
label: 'AI Profiles',
|
|
173
|
+
publicLabel: 'AI Agents',
|
|
174
|
+
config: { maxAiProfiles: 4 }
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
featureKey: 'context.uploads',
|
|
178
|
+
access: 'enabled',
|
|
179
|
+
label: 'Cloud Storage',
|
|
180
|
+
config: { storageLimitMb: 24 }
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
featureKey: 'collaboration.team_management',
|
|
184
|
+
access: 'enabled',
|
|
185
|
+
label: 'Team Management'
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
pricing: {
|
|
189
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: null, currency: null, interval: 'month' },
|
|
190
|
+
year: null
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
})
|
|
195
|
+
})));
|
|
196
|
+
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
197
|
+
const freeCard = await screen.findByTestId('plan-card-free');
|
|
198
|
+
const featureTextNodes = Array.from(freeCard.querySelectorAll('span')).map((node) => node.textContent);
|
|
199
|
+
expect(featureTextNodes).toContain('1x Workspaces');
|
|
200
|
+
expect(featureTextNodes).toContain('4x AI Agents');
|
|
201
|
+
expect(featureTextNodes).toContain('24MB Cloud Storage');
|
|
202
|
+
expect(featureTextNodes).toContain('12x Team Management');
|
|
203
|
+
expect(container.querySelector('[class*="featureLimitUnit"]')?.textContent).toBe('MB');
|
|
204
|
+
});
|
|
205
|
+
it('renders the storage unit suffix in a smaller accent span', async () => {
|
|
206
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
207
|
+
ok: true,
|
|
208
|
+
json: async () => ({
|
|
209
|
+
pricingSource: {
|
|
210
|
+
environment: 'production',
|
|
211
|
+
runtimeMode: 'cloud'
|
|
212
|
+
},
|
|
213
|
+
plans: [
|
|
214
|
+
{
|
|
215
|
+
planId: 'free',
|
|
216
|
+
displayName: 'Free',
|
|
217
|
+
status: 'active',
|
|
218
|
+
defaultVersion: {
|
|
219
|
+
planVersionId: 'free-v1',
|
|
220
|
+
versionNumber: 1,
|
|
221
|
+
seatLimit: null,
|
|
222
|
+
metadata: {}
|
|
223
|
+
},
|
|
224
|
+
features: [
|
|
225
|
+
{
|
|
226
|
+
featureKey: 'context.uploads',
|
|
227
|
+
access: 'enabled',
|
|
228
|
+
label: 'Cloud Storage',
|
|
229
|
+
config: { storageLimitMb: 64 }
|
|
230
|
+
}
|
|
231
|
+
],
|
|
232
|
+
pricing: {
|
|
233
|
+
month: { pricingType: 'free', stripePriceId: null, unitAmount: null, currency: null, interval: 'month' },
|
|
234
|
+
year: null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
})
|
|
239
|
+
})));
|
|
240
|
+
const { container } = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
241
|
+
await screen.findByText('Cloud Storage');
|
|
242
|
+
const accent = container.querySelector('[class*="featureLimitAccent"]');
|
|
243
|
+
const unit = container.querySelector('[class*="featureLimitUnit"]');
|
|
244
|
+
expect(accent?.textContent).toBe('64MB');
|
|
245
|
+
expect(unit?.textContent).toBe('MB');
|
|
246
|
+
});
|
|
247
|
+
it('hides missing yearly pricing rows and buttons instead of rendering unavailable state', async () => {
|
|
248
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({
|
|
249
|
+
ok: true,
|
|
250
|
+
json: async () => ({
|
|
251
|
+
pricingSource: {
|
|
252
|
+
environment: 'production',
|
|
253
|
+
runtimeMode: 'cloud'
|
|
254
|
+
},
|
|
255
|
+
plans: [
|
|
256
|
+
{
|
|
257
|
+
planId: 'operator',
|
|
258
|
+
displayName: 'Operator',
|
|
259
|
+
status: 'active',
|
|
260
|
+
defaultVersion: {
|
|
261
|
+
planVersionId: 'operator-v1',
|
|
262
|
+
versionNumber: 1,
|
|
263
|
+
seatLimit: null,
|
|
264
|
+
metadata: {}
|
|
265
|
+
},
|
|
266
|
+
features: [],
|
|
267
|
+
pricing: {
|
|
268
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_operator_month', unitAmount: 999, currency: 'usd', interval: 'month' },
|
|
269
|
+
year: null
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
})
|
|
274
|
+
})));
|
|
275
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
276
|
+
const operatorCard = await screen.findByTestId('plan-card-operator');
|
|
277
|
+
expect(within(operatorCard).getByText('$9.99')).toBeInTheDocument();
|
|
278
|
+
expect(within(operatorCard).getByText('/ month')).toBeInTheDocument();
|
|
279
|
+
expect(within(operatorCard).queryByText('/ year')).not.toBeInTheDocument();
|
|
280
|
+
expect(within(operatorCard).queryByText('Unavailable')).not.toBeInTheDocument();
|
|
281
|
+
expect(within(operatorCard).getByRole('button', { name: 'Choose Monthly' })).toBeInTheDocument();
|
|
282
|
+
expect(within(operatorCard).queryByRole('button', { name: 'Choose Yearly' })).not.toBeInTheDocument();
|
|
283
|
+
});
|
|
95
284
|
it('does not mark a plan current when only the plan id matches but the version does not', async () => {
|
|
96
285
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v1", currentEntitlementState: "active", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
97
286
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
@@ -102,15 +291,64 @@ describe('PlanComparisonPage', () => {
|
|
|
102
291
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
103
292
|
expect(screen.getByText('Current Plan')).toBeInTheDocument();
|
|
104
293
|
});
|
|
294
|
+
it('can show the current plan as a non-interactive indicator when billing is managed from page chrome', async () => {
|
|
295
|
+
const onSelectPlan = vi.fn();
|
|
296
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v2", currentEntitlementState: "active", canManageCurrentPlan: true, currentPlanCardActionMode: "indicator", onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
297
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
298
|
+
const currentCard = screen.getByTestId('plan-card-team-5');
|
|
299
|
+
expect(currentCard).not.toHaveAttribute('role', 'button');
|
|
300
|
+
expect(screen.queryByTestId('plan-manage-team-5')).not.toBeInTheDocument();
|
|
301
|
+
expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
|
|
302
|
+
expect(screen.getByTestId('plan-current-team-5')).toBeDisabled();
|
|
303
|
+
fireEvent.click(currentCard);
|
|
304
|
+
expect(onSelectPlan).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
it('can render the page header bar even when shell chrome is present', async () => {
|
|
307
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, chrome: _jsx("div", { children: "Shell chrome" }), showHeaderBarWithChrome: true, topActions: _jsx("button", { type: "button", children: "Manage billing" }), onBack: () => { }, backLabel: "Back to App", onSelectPlan: () => { } }));
|
|
308
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
309
|
+
expect(screen.getByRole('button', { name: 'Back to App' })).toBeInTheDocument();
|
|
310
|
+
expect(screen.getByRole('button', { name: 'Manage billing' })).toBeInTheDocument();
|
|
311
|
+
});
|
|
105
312
|
it('does not mark a canceled entitlement as current through the plan-id fallback path', async () => {
|
|
106
313
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: null, currentEntitlementState: "canceled", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
107
314
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
108
315
|
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
109
316
|
});
|
|
317
|
+
it('does not mark a linked pending-plan-selection entitlement as the current plan', async () => {
|
|
318
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v2", currentEntitlementState: "pending_plan_selection", canManageCurrentPlan: true, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
319
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
320
|
+
expect(screen.queryByRole('button', { name: 'Manage Plan' })).not.toBeInTheDocument();
|
|
321
|
+
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
322
|
+
expect(screen.getByRole('button', { name: 'Choose Monthly' })).toBeInTheDocument();
|
|
323
|
+
});
|
|
110
324
|
it('can disable the page back button while onboarding remains blocked', async () => {
|
|
111
325
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, onBack: () => { }, backLabel: "Back to App", backDisabled: true, onSelectPlan: () => { } }));
|
|
112
326
|
expect(await screen.findByRole('button', { name: 'Back to App' })).toBeDisabled();
|
|
113
327
|
});
|
|
328
|
+
it('shows pricing before auth resolves but keeps plan selection disabled until bootstrap finishes', async () => {
|
|
329
|
+
const onSelectPlan = vi.fn();
|
|
330
|
+
const view = render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
331
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
332
|
+
const freeCard = screen.getByTestId('plan-card-free');
|
|
333
|
+
expect(screen.getByRole('button', { name: 'Get Started Free' })).toBeDisabled();
|
|
334
|
+
expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeDisabled();
|
|
335
|
+
expect(freeCard).not.toHaveAttribute('role', 'button');
|
|
336
|
+
view.rerender(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, authSessionResolved: true, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
337
|
+
const enabledFreeCard = screen.getByTestId('plan-card-free');
|
|
338
|
+
expect(screen.getByRole('button', { name: 'Get Started Free' })).toBeEnabled();
|
|
339
|
+
expect(screen.getByRole('button', { name: 'Get Started Monthly' })).toBeEnabled();
|
|
340
|
+
expect(enabledFreeCard).toHaveAttribute('role', 'button');
|
|
341
|
+
fireEvent.click(screen.getByRole('button', { name: 'Get Started Free' }));
|
|
342
|
+
await waitFor(() => {
|
|
343
|
+
expect(onSelectPlan).toHaveBeenCalledWith({
|
|
344
|
+
planId: 'free',
|
|
345
|
+
planVersionId: 'free-v1',
|
|
346
|
+
interval: 'month',
|
|
347
|
+
pricingType: 'free',
|
|
348
|
+
isCurrentPlan: false
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
114
352
|
it('selects the whole plan card using the default available interval', async () => {
|
|
115
353
|
const onSelectPlan = vi.fn();
|
|
116
354
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|