@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.
Files changed (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. 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
- misconfigured: 'Signup monetization is misconfigured. Contact support or a system administrator.',
14
+ checkoutPending: 'Complete checkout to continue.',
15
+ misconfigured: 'Onboarding policy is misconfigured. Contact support or a system administrator.',
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 [billingStatus, setBillingStatus] = useState(null);
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(billingStatus?.entitlementState || '').trim().toLowerCase();
93
+ const entitlementState = String(accountProfileSummary?.entitlementState || '').trim().toLowerCase();
85
94
  return ACTIVE_ENTITLEMENT_STATES.has(entitlementState);
86
- }, [billingStatus?.entitlementState]);
95
+ }, [accountProfileSummary?.entitlementState]);
87
96
  const gateMessage = useMemo(() => {
88
97
  if (hasResolvedActiveEntitlement)
89
98
  return null;
90
- if (gateState === 'plan_selection_required')
99
+ const billingConflictCode = String(accountProfileSummary?.billingConflictCode || '').trim().toUpperCase();
100
+ if (billingConflictCode === 'CHECKOUT_SESSION_EXPIRED')
101
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED;
102
+ if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
103
+ return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
104
+ const summaryMessage = String(accountProfileSummary?.message || '').trim();
105
+ if (summaryMessage)
106
+ return summaryMessage;
107
+ const effectiveGateState = String(accountProfileSummary?.gate || gateState || '').trim().toLowerCase();
108
+ if (effectiveGateState === 'plan_selection_required')
91
109
  return GATE_MESSAGES.planSelectionRequired;
92
- if (gateState === 'misconfigured')
110
+ if (effectiveGateState === 'checkout_pending')
111
+ return GATE_MESSAGES.checkoutPending;
112
+ if (effectiveGateState === 'misconfigured')
93
113
  return GATE_MESSAGES.misconfigured;
94
- if (gateState === 'missing_entitlement')
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 loadBillingStatus = useCallback(async () => {
138
+ const loadAccountProfileSummary = useCallback(async (options) => {
139
+ const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);
119
140
  if (!isAuthenticated) {
120
- setBillingStatus(null);
121
- return;
141
+ setAccountProfileSummary(null);
142
+ invalidateAccountProfileSummaryCache(accountProfileSummaryUrl, { identityKey: authUserId });
143
+ return null;
122
144
  }
123
145
  try {
124
- const res = await fetch(buildApiUrl('/api/taskforce/billing/status'), {
125
- method: 'GET',
126
- credentials: 'include'
146
+ const payload = await loadAccountProfileSummaryCached(accountProfileSummaryUrl, {
147
+ identityKey: authUserId,
148
+ force: options?.force === true
127
149
  });
128
- if (!res.ok)
129
- return;
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
- }, [buildApiUrl, isAuthenticated]);
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(buildApiUrl('/api/taskforce/billing/checkout-session'), {
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
- await loadBillingStatus();
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
- await loadBillingStatus();
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
- await loadBillingStatus();
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
- }, [buildApiUrl, loadBillingStatus]);
238
+ }, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, runtimeMode]);
204
239
  const confirmCheckoutSession = useCallback(async (sessionId) => {
205
- if (!sessionId)
206
- return false;
240
+ if (!sessionId) {
241
+ return {
242
+ confirmed: false,
243
+ errorMessage: 'Checkout session is missing.'
244
+ };
245
+ }
207
246
  try {
208
- const res = await fetch(buildApiUrl('/api/taskforce/billing/checkout-session/confirm'), {
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
- setFeedback({ type: 'error', message: String(payload?.error || 'Unable to confirm checkout completion.') });
217
- return false;
255
+ return {
256
+ confirmed: false,
257
+ errorMessage: String(payload?.error || 'Unable to confirm checkout completion.')
258
+ };
218
259
  }
219
- await loadBillingStatus();
220
- return true;
260
+ invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
261
+ await loadAccountProfileSummary();
262
+ return { confirmed: true };
221
263
  }
222
264
  catch {
223
- setFeedback({ type: 'error', message: 'Unable to confirm checkout completion.' });
224
- return false;
265
+ return {
266
+ confirmed: false,
267
+ errorMessage: 'Unable to confirm checkout completion.'
268
+ };
225
269
  }
226
- }, [buildApiUrl, loadBillingStatus]);
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(buildApiUrl('/api/taskforce/billing/portal-session'), {
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
- }, [buildApiUrl, isAuthenticated, navigate]);
301
+ }, [buildCloudBillingUrl, isAuthenticated, navigate]);
258
302
  useEffect(() => {
259
- void loadBillingStatus();
260
- }, [loadBillingStatus]);
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 confirmed = querySessionId
322
+ const confirmation = querySessionId
279
323
  ? await confirmCheckoutSession(querySessionId)
280
- : false;
281
- if (!confirmed) {
282
- await loadBillingStatus();
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
- setFeedback({ type: 'error', message: 'Checkout was canceled. No charge was made.' });
291
- scheduleTransientQueryClear();
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, loadBillingStatus, queryInterval, queryPlanVersionId, querySessionId, scheduleTransientQueryClear, startCheckout]);
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 (!billingStatus?.stripeCustomerId) {
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 && billingStatus?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })) })) : (_jsxs(_Fragment, { children: [_jsx("button", { className: styles.navAction, onClick: handleBack, children: "Back" }), isAuthenticated && billingStatus?.stripeCustomerId && (_jsx("button", { className: styles.navAction, onClick: () => { void openBillingPortal(); }, disabled: actionBusy, children: "Manage billing" })), !isAuthenticated && (_jsxs("button", { className: styles.navAction, onClick: () => navigate(`/login?mode=login&next=${encodeURIComponent(buildEmbeddedPlansPath())}`), children: [_jsx(User, { size: 16, style: { marginRight: '8px', verticalAlign: 'text-bottom' } }), "Sign In"] }))] }));
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(billingStatus?.planId || queryPlanId || '').trim() || null, currentPlanVersionId: String(billingStatus?.planVersionId || queryPlanVersionId || '').trim() || null, currentEntitlementState: String(billingStatus?.entitlementState || '').trim() || null, canManageCurrentPlan: Boolean(billingStatus?.stripeCustomerId), pricingEndpoint: pricingEndpoints[0], statusBanner: feedback, onBack: handleBack, backLabel: embedded ? 'Back to Board' : 'Back to App', topActions: embedded ? topActions : null, shellOwnsScroll: shellOwnsScroll, showPageBackButton: !embedded, fallbackPricingSource: fallbackPricingSource, onSelectPlan: handleSelectPlan }));
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;