@taskforcehq/taskforce 0.3.301 → 0.3.303
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 +37 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +72 -33
- package/dist/TaskforceCore.test.js +848 -130
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/task/TaskKanban.test.js +17 -0
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.d.ts +3 -1
- package/dist/components/views/PlansPage.js +109 -49
- package/dist/components/views/PlansPage.test.d.ts +1 -0
- package/dist/components/views/PlansPage.test.js +100 -0
- package/dist/components/views/StandaloneLayout.d.ts +1 -0
- package/dist/components/views/StandaloneLayout.js +473 -256
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +78 -18
- package/dist/core/PlanEntitlementService.d.ts +29 -5
- package/dist/core/PlanEntitlementService.js +297 -219
- package/dist/core/SignupMonetizationSettings.test.js +173 -49
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/SystemAdmin.test.js +186 -96
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +100 -10
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +459 -127
- package/dist/core/Taskforce.mcpAuth.test.js +0 -2
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +13 -12
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useSyncOrchestrator.js +17 -3
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +8 -0
- package/dist/hooks/useTaskforce.js +72 -19
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +131 -48
- package/dist/mcp/runtime.test.js +321 -83
- package/dist/migrations/billingSchemaParity.test.js +3 -0
- package/dist/migrations/taskSchemaMigrations.js +68 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +17 -1
- package/dist/server/index.test.js +15 -1
- package/dist/server/mockStripe.d.ts +1 -0
- package/dist/server/mockStripe.js +2 -0
- package/dist/server/routes/admin.js +105 -45
- package/dist/server/routes/auth.d.ts +6 -0
- package/dist/server/routes/auth.js +505 -30
- package/dist/server/routes/billing.js +517 -24
- package/dist/server/routes/billing.test.js +967 -45
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +16 -6
- package/dist/server/routes/sync.integration.test.js +290 -0
- package/dist/server/routes/sync.js +234 -1922
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +29 -22
- package/dist/server/routes.test.js +639 -82
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/cloudSyncApi.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +98 -0
- package/dist/sync/contentSyncState.js +1004 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +21 -8
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +38 -0
- package/dist/sync/workspacePullFeed.js +349 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +278 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
- package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
- package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
- package/dist/ui/assets/index-CLIMgR6g.js +6 -0
- package/dist/ui/assets/index-DneETxcu.css +1 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
- package/dist/utils/accountProfileSummaryCache.js +97 -0
- package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
- package/dist/utils/accountProfileSummaryCache.test.js +63 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +4 -3
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- package/dist/ui/assets/index-xBWLPOEb.js +0 -6
|
@@ -6,13 +6,19 @@ import styles from './PlansPage.module.css';
|
|
|
6
6
|
import { PlanComparisonPage } from './PlanComparisonPage';
|
|
7
7
|
import { AppShellHeader } from './AppShellHeader';
|
|
8
8
|
import { resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
|
|
9
|
+
import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from '../../utils/accountProfileSummaryCache';
|
|
9
10
|
const TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS = 1600;
|
|
10
11
|
const ACTIVE_ENTITLEMENT_STATES = new Set(['active', 'trialing', 'grace']);
|
|
11
12
|
const GATE_MESSAGES = {
|
|
12
13
|
planSelectionRequired: 'Choose a plan to continue.',
|
|
13
|
-
|
|
14
|
+
checkoutPending: 'Complete checkout to continue.',
|
|
15
|
+
misconfigured: 'Onboarding policy is misconfigured. Contact support or a system administrator.',
|
|
14
16
|
missingEntitlement: 'No entitlement is linked to this account.'
|
|
15
17
|
};
|
|
18
|
+
const BILLING_CONFLICT_MESSAGES = {
|
|
19
|
+
CHECKOUT_SESSION_EXPIRED: 'Your previous checkout expired. Choose a plan to continue.',
|
|
20
|
+
CHECKOUT_SESSION_SUPERSEDED: 'A newer checkout replaced your previous checkout. Choose a plan to continue.'
|
|
21
|
+
};
|
|
16
22
|
function parseInterval(raw) {
|
|
17
23
|
return raw === 'year' ? 'year' : 'month';
|
|
18
24
|
}
|
|
@@ -32,10 +38,10 @@ function buildEmbeddedPlansPath(params = {}) {
|
|
|
32
38
|
}
|
|
33
39
|
return `/?${query.toString()}`;
|
|
34
40
|
}
|
|
35
|
-
export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, currentTheme = 'dark', projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, embedded = false, shellOwnsScroll = false }) {
|
|
41
|
+
export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessionResolved, currentTheme = 'dark', projectName, currentWorkspaceId, apiBaseUrl, connectedEnvironmentSource, resolveCloudAuthUrl, embedded = false, shellOwnsScroll = false }) {
|
|
36
42
|
const navigate = useNavigate();
|
|
37
43
|
const location = useLocation();
|
|
38
|
-
const [
|
|
44
|
+
const [accountProfileSummary, setAccountProfileSummary] = useState(null);
|
|
39
45
|
const [actionBusy, setActionBusy] = useState(false);
|
|
40
46
|
const [feedback, setFeedback] = useState(null);
|
|
41
47
|
const checkoutStartRan = useRef(false);
|
|
@@ -62,6 +68,9 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
62
68
|
return normalizedPath;
|
|
63
69
|
return `${normalizedApiBaseUrl}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`;
|
|
64
70
|
}, [normalizedApiBaseUrl]);
|
|
71
|
+
const buildCloudBillingUrl = useCallback((path) => {
|
|
72
|
+
return resolveCloudAuthUrl ? resolveCloudAuthUrl(path) : buildApiUrl(path);
|
|
73
|
+
}, [buildApiUrl, resolveCloudAuthUrl]);
|
|
65
74
|
const closeEmbeddedPlansScreen = useCallback(() => {
|
|
66
75
|
const params = new URLSearchParams(location.search);
|
|
67
76
|
params.delete('screen');
|
|
@@ -81,20 +90,31 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
81
90
|
return endpoints;
|
|
82
91
|
}, [normalizedApiBaseUrl]);
|
|
83
92
|
const hasResolvedActiveEntitlement = useMemo(() => {
|
|
84
|
-
const entitlementState = String(
|
|
93
|
+
const entitlementState = String(accountProfileSummary?.entitlementState || '').trim().toLowerCase();
|
|
85
94
|
return ACTIVE_ENTITLEMENT_STATES.has(entitlementState);
|
|
86
|
-
}, [
|
|
95
|
+
}, [accountProfileSummary?.entitlementState]);
|
|
87
96
|
const gateMessage = useMemo(() => {
|
|
88
97
|
if (hasResolvedActiveEntitlement)
|
|
89
98
|
return null;
|
|
90
|
-
|
|
99
|
+
const billingConflictCode = String(accountProfileSummary?.billingConflictCode || '').trim().toUpperCase();
|
|
100
|
+
if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
|
|
101
|
+
return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED;
|
|
102
|
+
if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
|
|
103
|
+
return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
|
|
104
|
+
const summaryMessage = String(accountProfileSummary?.message || '').trim();
|
|
105
|
+
if (summaryMessage)
|
|
106
|
+
return summaryMessage;
|
|
107
|
+
const effectiveGateState = String(accountProfileSummary?.gate || gateState || '').trim().toLowerCase();
|
|
108
|
+
if (effectiveGateState === 'plan_selection_required')
|
|
91
109
|
return GATE_MESSAGES.planSelectionRequired;
|
|
92
|
-
if (
|
|
110
|
+
if (effectiveGateState === 'checkout_pending')
|
|
111
|
+
return GATE_MESSAGES.checkoutPending;
|
|
112
|
+
if (effectiveGateState === 'misconfigured')
|
|
93
113
|
return GATE_MESSAGES.misconfigured;
|
|
94
|
-
if (
|
|
114
|
+
if (effectiveGateState === 'missing_entitlement')
|
|
95
115
|
return GATE_MESSAGES.missingEntitlement;
|
|
96
116
|
return null;
|
|
97
|
-
}, [gateState, hasResolvedActiveEntitlement]);
|
|
117
|
+
}, [accountProfileSummary?.billingConflictCode, accountProfileSummary?.gate, accountProfileSummary?.message, gateState, hasResolvedActiveEntitlement]);
|
|
98
118
|
const clearTransientQueryState = useCallback(() => {
|
|
99
119
|
const next = new URLSearchParams(location.search);
|
|
100
120
|
next.delete('checkout');
|
|
@@ -115,25 +135,37 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
115
135
|
transientQueryClearTimeoutRef.current = null;
|
|
116
136
|
}, TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS);
|
|
117
137
|
}, [clearTransientQueryState]);
|
|
118
|
-
const
|
|
138
|
+
const loadAccountProfileSummary = useCallback(async (options) => {
|
|
139
|
+
const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);
|
|
119
140
|
if (!isAuthenticated) {
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
setAccountProfileSummary(null);
|
|
142
|
+
invalidateAccountProfileSummaryCache(accountProfileSummaryUrl, { identityKey: authUserId });
|
|
143
|
+
return null;
|
|
122
144
|
}
|
|
123
145
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
146
|
+
const payload = await loadAccountProfileSummaryCached(accountProfileSummaryUrl, {
|
|
147
|
+
identityKey: authUserId,
|
|
148
|
+
force: options?.force === true
|
|
127
149
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const payload = await res.json().catch(() => ({}));
|
|
131
|
-
setBillingStatus(payload || null);
|
|
150
|
+
setAccountProfileSummary(payload);
|
|
151
|
+
return payload;
|
|
132
152
|
}
|
|
133
153
|
catch {
|
|
134
154
|
// best effort only
|
|
155
|
+
return null;
|
|
135
156
|
}
|
|
136
|
-
}, [
|
|
157
|
+
}, [authUserId, buildCloudBillingUrl, isAuthenticated]);
|
|
158
|
+
const resolveRecoveryMessage = useCallback((summary, fallback) => {
|
|
159
|
+
const billingConflictCode = String(summary?.billingConflictCode || '').trim().toUpperCase();
|
|
160
|
+
if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
|
|
161
|
+
return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED;
|
|
162
|
+
if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
|
|
163
|
+
return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
|
|
164
|
+
const summaryMessage = String(summary?.message || '').trim();
|
|
165
|
+
if (summaryMessage)
|
|
166
|
+
return summaryMessage;
|
|
167
|
+
return String(fallback || 'Unable to confirm checkout completion.').trim() || 'Unable to confirm checkout completion.';
|
|
168
|
+
}, []);
|
|
137
169
|
const startCheckout = useCallback(async (planVersionId, interval) => {
|
|
138
170
|
if (!planVersionId) {
|
|
139
171
|
setFeedback({ type: 'error', message: 'Selected plan is missing a default plan version.' });
|
|
@@ -142,7 +174,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
142
174
|
setActionBusy(true);
|
|
143
175
|
setFeedback(null);
|
|
144
176
|
try {
|
|
145
|
-
const res = await fetch(
|
|
177
|
+
const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/checkout-session'), {
|
|
146
178
|
method: 'POST',
|
|
147
179
|
credentials: 'include',
|
|
148
180
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -156,17 +188,20 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
156
188
|
const checkoutState = String(payload?.checkoutState || '').trim().toLowerCase();
|
|
157
189
|
if (checkoutState === 'free') {
|
|
158
190
|
setFeedback({ type: 'success', message: 'Plan selected. Your access is ready.' });
|
|
159
|
-
|
|
191
|
+
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
192
|
+
await loadAccountProfileSummary();
|
|
160
193
|
return;
|
|
161
194
|
}
|
|
162
195
|
if (checkoutState === 'updated') {
|
|
163
196
|
setFeedback({ type: 'success', message: String(payload?.message || 'Your subscription was updated successfully.') });
|
|
164
|
-
|
|
197
|
+
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
198
|
+
await loadAccountProfileSummary();
|
|
165
199
|
return;
|
|
166
200
|
}
|
|
167
201
|
if (checkoutState === 'already_current_plan') {
|
|
168
202
|
setFeedback({ type: 'success', message: String(payload?.message || 'You are already on this plan.') });
|
|
169
|
-
|
|
203
|
+
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
204
|
+
await loadAccountProfileSummary();
|
|
170
205
|
return;
|
|
171
206
|
}
|
|
172
207
|
const returnBaseUrl = String(payload?.returnBaseUrl || '').trim();
|
|
@@ -200,12 +235,16 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
200
235
|
finally {
|
|
201
236
|
setActionBusy(false);
|
|
202
237
|
}
|
|
203
|
-
}, [
|
|
238
|
+
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, runtimeMode]);
|
|
204
239
|
const confirmCheckoutSession = useCallback(async (sessionId) => {
|
|
205
|
-
if (!sessionId)
|
|
206
|
-
return
|
|
240
|
+
if (!sessionId) {
|
|
241
|
+
return {
|
|
242
|
+
confirmed: false,
|
|
243
|
+
errorMessage: 'Checkout session is missing.'
|
|
244
|
+
};
|
|
245
|
+
}
|
|
207
246
|
try {
|
|
208
|
-
const res = await fetch(
|
|
247
|
+
const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/checkout-session/confirm'), {
|
|
209
248
|
method: 'POST',
|
|
210
249
|
credentials: 'include',
|
|
211
250
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -213,17 +252,22 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
213
252
|
});
|
|
214
253
|
const payload = await res.json().catch(() => ({}));
|
|
215
254
|
if (!res.ok) {
|
|
216
|
-
|
|
217
|
-
|
|
255
|
+
return {
|
|
256
|
+
confirmed: false,
|
|
257
|
+
errorMessage: String(payload?.error || 'Unable to confirm checkout completion.')
|
|
258
|
+
};
|
|
218
259
|
}
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
261
|
+
await loadAccountProfileSummary();
|
|
262
|
+
return { confirmed: true };
|
|
221
263
|
}
|
|
222
264
|
catch {
|
|
223
|
-
|
|
224
|
-
|
|
265
|
+
return {
|
|
266
|
+
confirmed: false,
|
|
267
|
+
errorMessage: 'Unable to confirm checkout completion.'
|
|
268
|
+
};
|
|
225
269
|
}
|
|
226
|
-
}, [
|
|
270
|
+
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary]);
|
|
227
271
|
const openBillingPortal = useCallback(async () => {
|
|
228
272
|
if (!isAuthenticated) {
|
|
229
273
|
navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`);
|
|
@@ -232,7 +276,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
232
276
|
setActionBusy(true);
|
|
233
277
|
setFeedback(null);
|
|
234
278
|
try {
|
|
235
|
-
const res = await fetch(
|
|
279
|
+
const res = await fetch(buildCloudBillingUrl('/api/taskforce/billing/portal-session'), {
|
|
236
280
|
method: 'POST',
|
|
237
281
|
credentials: 'include'
|
|
238
282
|
});
|
|
@@ -254,10 +298,10 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
254
298
|
finally {
|
|
255
299
|
setActionBusy(false);
|
|
256
300
|
}
|
|
257
|
-
}, [
|
|
301
|
+
}, [buildCloudBillingUrl, isAuthenticated, navigate]);
|
|
258
302
|
useEffect(() => {
|
|
259
|
-
void
|
|
260
|
-
}, [
|
|
303
|
+
void loadAccountProfileSummary();
|
|
304
|
+
}, [loadAccountProfileSummary]);
|
|
261
305
|
useEffect(() => {
|
|
262
306
|
if (!hasResolvedActiveEntitlement)
|
|
263
307
|
return;
|
|
@@ -275,11 +319,17 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
275
319
|
checkoutStartRan.current = true;
|
|
276
320
|
setFeedback({ type: 'success', message: 'Checkout completed successfully. Finalizing your plan...' });
|
|
277
321
|
void (async () => {
|
|
278
|
-
const
|
|
322
|
+
const confirmation = querySessionId
|
|
279
323
|
? await confirmCheckoutSession(querySessionId)
|
|
280
|
-
: false;
|
|
281
|
-
if (!confirmed) {
|
|
282
|
-
await
|
|
324
|
+
: { confirmed: false, errorMessage: 'Checkout session is missing.' };
|
|
325
|
+
if (!confirmation.confirmed) {
|
|
326
|
+
const summary = await loadAccountProfileSummary({ force: true });
|
|
327
|
+
setFeedback({
|
|
328
|
+
type: 'error',
|
|
329
|
+
message: resolveRecoveryMessage(summary, confirmation.errorMessage)
|
|
330
|
+
});
|
|
331
|
+
scheduleTransientQueryClear();
|
|
332
|
+
return;
|
|
283
333
|
}
|
|
284
334
|
setFeedback({ type: 'success', message: 'Checkout completed successfully.' });
|
|
285
335
|
scheduleTransientQueryClear();
|
|
@@ -287,8 +337,14 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
287
337
|
return;
|
|
288
338
|
}
|
|
289
339
|
if (checkoutState === 'cancel') {
|
|
290
|
-
|
|
291
|
-
|
|
340
|
+
void (async () => {
|
|
341
|
+
const summary = await loadAccountProfileSummary({ force: true });
|
|
342
|
+
setFeedback({
|
|
343
|
+
type: 'error',
|
|
344
|
+
message: resolveRecoveryMessage(summary, 'Checkout was canceled. No charge was made.')
|
|
345
|
+
});
|
|
346
|
+
scheduleTransientQueryClear();
|
|
347
|
+
})();
|
|
292
348
|
return;
|
|
293
349
|
}
|
|
294
350
|
if ((checkoutState === 'start' || checkoutState === 'free') && isAuthenticated && queryPlanVersionId && !checkoutStartRan.current) {
|
|
@@ -309,9 +365,13 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
309
365
|
return null;
|
|
310
366
|
if (current.message === GATE_MESSAGES.missingEntitlement)
|
|
311
367
|
return null;
|
|
368
|
+
if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED)
|
|
369
|
+
return null;
|
|
370
|
+
if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED)
|
|
371
|
+
return null;
|
|
312
372
|
return current;
|
|
313
373
|
});
|
|
314
|
-
}, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated,
|
|
374
|
+
}, [checkoutState, confirmCheckoutSession, gateMessage, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveRecoveryMessage, scheduleTransientQueryClear, startCheckout]);
|
|
315
375
|
const handleSelectPlan = useCallback(async (intent) => {
|
|
316
376
|
if (!isAuthenticated) {
|
|
317
377
|
const nextCheckoutState = intent.pricingType === 'free' ? 'free' : 'start';
|
|
@@ -325,7 +385,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
325
385
|
return;
|
|
326
386
|
}
|
|
327
387
|
if (intent.isCurrentPlan) {
|
|
328
|
-
if (!
|
|
388
|
+
if (!accountProfileSummary?.stripeCustomerId) {
|
|
329
389
|
setFeedback({ type: 'success', message: 'Your current plan does not use Stripe billing.' });
|
|
330
390
|
return;
|
|
331
391
|
}
|
|
@@ -349,7 +409,7 @@ export function PlansPage({ isAuthenticated, runtimeMode, authSessionResolved, c
|
|
|
349
409
|
}
|
|
350
410
|
navigate('/');
|
|
351
411
|
}, [closeEmbeddedPlansScreen, embedded, navigate]);
|
|
352
|
-
const topActions = embedded ? (_jsx(_Fragment, { children: isAuthenticated &&
|
|
412
|
+
const topActions = embedded ? (_jsx(_Fragment, { children: 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, children: "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"] }))] }));
|
|
353
413
|
const chrome = embedded ? null : (_jsx(AppShellHeader, { projectName: projectName, currentWorkspaceId: currentWorkspaceId, runtimeMode: runtimeMode === 'cloud' ? 'cloud' : 'local', theme: currentTheme, actions: topActions }));
|
|
354
|
-
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(
|
|
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 }));
|
|
355
415
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
import { PlansPage } from './PlansPage';
|
|
6
|
+
vi.mock('./PlanComparisonPage', () => ({
|
|
7
|
+
PlanComparisonPage: ({ heading, statusBanner }) => (_jsxs("div", { children: [_jsx("h1", { children: heading }), statusBanner?.message ? _jsx("div", { children: statusBanner.message }) : null] }))
|
|
8
|
+
}));
|
|
9
|
+
describe('PlansPage', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.unstubAllGlobals();
|
|
12
|
+
});
|
|
13
|
+
it('shows recovery messaging instead of false checkout success when confirmation fails', async () => {
|
|
14
|
+
const fetchMock = vi.fn()
|
|
15
|
+
.mockResolvedValueOnce({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: async () => ({
|
|
18
|
+
planId: 'pro',
|
|
19
|
+
planVersionId: 'pro-v1',
|
|
20
|
+
entitlementState: 'checkout_pending',
|
|
21
|
+
gate: 'checkout_pending',
|
|
22
|
+
message: 'Complete checkout to continue.'
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
.mockResolvedValueOnce({
|
|
26
|
+
ok: false,
|
|
27
|
+
json: async () => ({
|
|
28
|
+
error: 'Checkout session has not completed yet.',
|
|
29
|
+
code: 'CHECKOUT_SESSION_INCOMPLETE'
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: async () => ({
|
|
35
|
+
planId: 'pro',
|
|
36
|
+
planVersionId: 'pro-v1',
|
|
37
|
+
entitlementState: 'checkout_pending',
|
|
38
|
+
gate: 'plan_selection_required',
|
|
39
|
+
message: 'Your previous checkout expired. Choose a plan to continue.',
|
|
40
|
+
planSelectionRequired: true
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
44
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&checkout=success&session_id=cs_test_123'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u1", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path }) }));
|
|
45
|
+
expect(await screen.findByText('Checkout completed successfully. Finalizing your plan...')).toBeInTheDocument();
|
|
46
|
+
expect(await screen.findByText('Your previous checkout expired. Choose a plan to continue.')).toBeInTheDocument();
|
|
47
|
+
expect(screen.queryByText('Checkout completed successfully.')).not.toBeInTheDocument();
|
|
48
|
+
await waitFor(() => {
|
|
49
|
+
expect(fetchMock).toHaveBeenNthCalledWith(2, '/api/taskforce/billing/checkout-session/confirm', expect.objectContaining({
|
|
50
|
+
method: 'POST'
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('shows conflict-specific recovery messaging when the account lands on plans already blocked', async () => {
|
|
55
|
+
const fetchMock = vi.fn()
|
|
56
|
+
.mockResolvedValueOnce({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: async () => ({
|
|
59
|
+
planId: 'pro',
|
|
60
|
+
planVersionId: 'pro-v1',
|
|
61
|
+
entitlementState: 'pending_plan_selection',
|
|
62
|
+
gate: 'plan_selection_required',
|
|
63
|
+
message: 'Choose a plan to continue.',
|
|
64
|
+
billingConflictCode: 'CHECKOUT_SESSION_SUPERSEDED',
|
|
65
|
+
planSelectionRequired: true
|
|
66
|
+
})
|
|
67
|
+
});
|
|
68
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
69
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u2", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path }) }));
|
|
70
|
+
expect(await screen.findByText('A newer checkout replaced your previous checkout. Choose a plan to continue.')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('shows conflict-specific recovery messaging after checkout cancel when the latest attempt expired', async () => {
|
|
73
|
+
const fetchMock = vi.fn()
|
|
74
|
+
.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: async () => ({
|
|
77
|
+
planId: 'pro',
|
|
78
|
+
planVersionId: 'pro-v1',
|
|
79
|
+
entitlementState: 'checkout_pending',
|
|
80
|
+
gate: 'checkout_pending',
|
|
81
|
+
message: 'Complete checkout to continue.'
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
.mockResolvedValueOnce({
|
|
85
|
+
ok: true,
|
|
86
|
+
json: async () => ({
|
|
87
|
+
planId: 'pro',
|
|
88
|
+
planVersionId: 'pro-v1',
|
|
89
|
+
entitlementState: 'pending_plan_selection',
|
|
90
|
+
gate: 'plan_selection_required',
|
|
91
|
+
message: 'Choose a plan to continue.',
|
|
92
|
+
billingConflictCode: 'CHECKOUT_SESSION_EXPIRED',
|
|
93
|
+
planSelectionRequired: true
|
|
94
|
+
})
|
|
95
|
+
});
|
|
96
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
97
|
+
render(_jsx(MemoryRouter, { initialEntries: ['/?screen=plans&checkout=cancel'], children: _jsx(PlansPage, { isAuthenticated: true, authUserId: "u3", runtimeMode: "cloud", authSessionResolved: true, resolveCloudAuthUrl: (path) => path }) }));
|
|
98
|
+
expect(await screen.findByText('Your previous checkout expired. Choose a plan to continue.')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type UseTaskforceResult } from '../../hooks/useTaskforce';
|
|
3
3
|
export type StandaloneLayoutProps = UseTaskforceResult & {
|
|
4
|
+
resolveCloudAuthUrl?: (path: string) => string;
|
|
4
5
|
onHeaderMouseDown?: (e: React.MouseEvent) => void;
|
|
5
6
|
isDragging?: boolean;
|
|
6
7
|
onClose?: () => void;
|