@taskforcehq/taskforce 0.3.302 → 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 (77) hide show
  1. package/dist/Taskforce.module.css +11 -0
  2. package/dist/TaskforceCore.js +59 -9
  3. package/dist/TaskforceCore.test.js +717 -14
  4. package/dist/components/task/TaskKanban.test.js +17 -0
  5. package/dist/components/views/PlansPage.d.ts +3 -1
  6. package/dist/components/views/PlansPage.js +110 -51
  7. package/dist/components/views/PlansPage.test.d.ts +1 -0
  8. package/dist/components/views/PlansPage.test.js +100 -0
  9. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  10. package/dist/components/views/StandaloneLayout.js +106 -151
  11. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  12. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  13. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  14. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  15. package/dist/core/EntitlementsPolicy.test.js +2 -2
  16. package/dist/core/GlobalSettingsService.js +78 -18
  17. package/dist/core/PlanEntitlementService.d.ts +29 -5
  18. package/dist/core/PlanEntitlementService.js +297 -219
  19. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  20. package/dist/core/SystemAdmin.test.js +186 -96
  21. package/dist/core/Taskforce.d.ts +51 -10
  22. package/dist/core/Taskforce.js +218 -49
  23. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  24. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  25. package/dist/core/types.d.ts +11 -12
  26. package/dist/hooks/useSyncOrchestrator.js +17 -3
  27. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  28. package/dist/hooks/useTaskforce.d.ts +3 -0
  29. package/dist/hooks/useTaskforce.js +9 -4
  30. package/dist/mcp/runtime.js +90 -29
  31. package/dist/mcp/runtime.test.js +272 -81
  32. package/dist/migrations/billingSchemaParity.test.js +3 -0
  33. package/dist/migrations/taskSchemaMigrations.js +15 -0
  34. package/dist/server/index.cookieProxy.test.js +1 -0
  35. package/dist/server/index.js +2 -0
  36. package/dist/server/index.test.js +2 -1
  37. package/dist/server/mockStripe.d.ts +1 -0
  38. package/dist/server/mockStripe.js +2 -0
  39. package/dist/server/routes/admin.js +80 -23
  40. package/dist/server/routes/auth.d.ts +1 -0
  41. package/dist/server/routes/auth.js +194 -53
  42. package/dist/server/routes/billing.js +506 -23
  43. package/dist/server/routes/billing.test.js +937 -45
  44. package/dist/server/routes/shared.js +5 -3
  45. package/dist/server/routes/sync.integration.test.js +160 -0
  46. package/dist/server/routes/sync.js +15 -5
  47. package/dist/server/routes.js +3 -3
  48. package/dist/server/routes.test.js +287 -76
  49. package/dist/sync/cloudSyncApi.d.ts +1 -0
  50. package/dist/sync/contentSyncState.d.ts +10 -0
  51. package/dist/sync/contentSyncState.js +277 -16
  52. package/dist/sync/syncService.d.ts +1 -0
  53. package/dist/sync/syncService.js +21 -8
  54. package/dist/sync/workspacePullFeed.d.ts +10 -0
  55. package/dist/sync/workspacePullFeed.js +111 -63
  56. package/dist/sync/workspacePullFeed.test.js +112 -0
  57. package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
  58. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  59. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
  60. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  61. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  62. package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
  63. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  64. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  65. package/dist/ui/assets/index-DneETxcu.css +1 -0
  66. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  69. package/dist/utils/accountProfileSummaryCache.js +97 -0
  70. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  71. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  72. package/package.json +3 -3
  73. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  74. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  75. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  76. package/dist/utils/billingStatusCache.d.ts +0 -21
  77. package/dist/utils/billingStatusCache.js +0 -70
@@ -48,6 +48,7 @@ function jsonResponse(body, status = 200) {
48
48
  describe('TaskforceCore Integration', () => {
49
49
  beforeEach(() => {
50
50
  localStorage.clear();
51
+ sessionStorage.clear();
51
52
  vi.stubGlobal('fetch', vi.fn((url) => {
52
53
  if (url.includes('/api/taskforce/auth/runtime-config')) {
53
54
  return Promise.resolve({ ok: true, json: () => Promise.resolve({ config: { runtimeMode: 'local', workspaceMode: 'single-local', authSource: 'cloud' } }) });
@@ -1037,7 +1038,7 @@ describe('TaskforceCore Integration', () => {
1037
1038
  })
1038
1039
  });
1039
1040
  }
1040
- if (url.includes('/api/taskforce/billing/status')) {
1041
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1041
1042
  return Promise.resolve({
1042
1043
  ok: true,
1043
1044
  json: () => Promise.resolve({
@@ -1047,7 +1048,7 @@ describe('TaskforceCore Integration', () => {
1047
1048
  })
1048
1049
  });
1049
1050
  }
1050
- if (url.includes('/api/taskforce/billing/status')) {
1051
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1051
1052
  return Promise.resolve({
1052
1053
  ok: true,
1053
1054
  json: () => Promise.resolve({
@@ -1143,7 +1144,7 @@ describe('TaskforceCore Integration', () => {
1143
1144
  hasEntitlement: false,
1144
1145
  canAccessApp: false,
1145
1146
  canAccessPlans: true,
1146
- message: 'Signup monetization is misconfigured.'
1147
+ message: 'Onboarding policy is misconfigured.'
1147
1148
  })
1148
1149
  });
1149
1150
  }
@@ -1196,7 +1197,7 @@ describe('TaskforceCore Integration', () => {
1196
1197
  render(_jsx(MemoryRouter, { initialEntries: ['/'], children: _jsx(TaskforceCore, {}) }));
1197
1198
  await waitFor(() => {
1198
1199
  expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1199
- expect(screen.getByText('Signup monetization is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
+ expect(screen.getByText('Onboarding policy is misconfigured. Contact support or a system administrator.')).toBeInTheDocument();
1200
1201
  });
1201
1202
  });
1202
1203
  it('suppresses stale missing-entitlement gate messaging when billing status is already active', async () => {
@@ -1287,7 +1288,7 @@ describe('TaskforceCore Integration', () => {
1287
1288
  })
1288
1289
  });
1289
1290
  }
1290
- if (url.includes('/api/taskforce/billing/status')) {
1291
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1291
1292
  return Promise.resolve({
1292
1293
  ok: true,
1293
1294
  json: () => Promise.resolve({
@@ -1388,7 +1389,7 @@ describe('TaskforceCore Integration', () => {
1388
1389
  })
1389
1390
  });
1390
1391
  }
1391
- if (url.includes('/api/taskforce/billing/status')) {
1392
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1392
1393
  return Promise.resolve({
1393
1394
  ok: true,
1394
1395
  json: () => Promise.resolve({
@@ -1445,7 +1446,7 @@ describe('TaskforceCore Integration', () => {
1445
1446
  await waitFor(() => {
1446
1447
  expect(screen.queryByText(/Signed in as/i)).not.toBeInTheDocument();
1447
1448
  expect(screen.getByText('member@example.com')).toBeInTheDocument();
1448
- expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/billing/status'))).toBe(true);
1449
+ expect(fetchMock.mock.calls.some((call) => String(call[0]).includes('/api/taskforce/account/profile-summary'))).toBe(true);
1449
1450
  });
1450
1451
  await waitFor(() => {
1451
1452
  expect(screen.getByText('Subscription')).toBeInTheDocument();
@@ -1507,7 +1508,7 @@ describe('TaskforceCore Integration', () => {
1507
1508
  })
1508
1509
  });
1509
1510
  }
1510
- if (url.includes('/api/taskforce/billing/status')) {
1511
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1511
1512
  return Promise.resolve({
1512
1513
  ok: true,
1513
1514
  json: () => Promise.resolve({
@@ -1569,6 +1570,292 @@ describe('TaskforceCore Integration', () => {
1569
1570
  expect(screen.getByText('CLOUD.EXAMPLE.COM')).toBeInTheDocument();
1570
1571
  });
1571
1572
  }, 10000);
1573
+ it('preserves the original user billing and team access after registering and switching back in local runtime', async () => {
1574
+ const user = userEvent.setup();
1575
+ const accounts = {
1576
+ ross: {
1577
+ email: 'ross.burke@gmail.com',
1578
+ userId: 'user-ross',
1579
+ displayName: 'Ross Burke',
1580
+ workspaceId: 'workspace-local-active',
1581
+ planId: 'team-5',
1582
+ planVersionId: 'team-5-v1',
1583
+ planName: 'Team 5',
1584
+ teamManagementAllowed: true,
1585
+ teamPlanMode: 'team',
1586
+ workspaceRole: 'owner'
1587
+ },
1588
+ newcomer: {
1589
+ email: 'new.user@example.com',
1590
+ userId: 'user-new',
1591
+ displayName: 'New User',
1592
+ workspaceId: 'workspace-local-active',
1593
+ planId: 'starter',
1594
+ planVersionId: 'starter-v1',
1595
+ planName: 'Starter',
1596
+ teamManagementAllowed: false,
1597
+ teamPlanMode: 'personal',
1598
+ workspaceRole: 'member'
1599
+ }
1600
+ };
1601
+ let currentAccountKey = 'ross';
1602
+ const currentAccount = () => (currentAccountKey ? accounts[currentAccountKey] : null);
1603
+ const fetchMock = vi.fn((input, init) => {
1604
+ const url = String(input);
1605
+ const account = currentAccount();
1606
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1607
+ return jsonResponse({
1608
+ success: true,
1609
+ config: {
1610
+ baseUrl: 'https://cloud.example.com',
1611
+ apiBaseUrl: 'https://cloud.example.com',
1612
+ cloudAuthBaseUrl: 'https://cloud.example.com',
1613
+ runtimeMode: 'local',
1614
+ authSource: 'cloud',
1615
+ workspaceMode: 'single-local',
1616
+ workspaceSwitchingEnabled: false,
1617
+ cloudAuthViaLocalProxy: false
1618
+ }
1619
+ });
1620
+ }
1621
+ if (url.includes('/api/taskforce/auth/session')) {
1622
+ return jsonResponse({
1623
+ authenticated: Boolean(account),
1624
+ runtimeMode: 'local',
1625
+ authRequiredForApi: false,
1626
+ email: account?.email || '',
1627
+ displayName: account?.displayName || '',
1628
+ userId: account?.userId || 'anonymous',
1629
+ workspaceId: account?.workspaceId || '',
1630
+ betaAccess: true
1631
+ });
1632
+ }
1633
+ if (url.includes('/api/taskforce/auth/logout')) {
1634
+ currentAccountKey = null;
1635
+ return jsonResponse({ success: true });
1636
+ }
1637
+ if (url.includes('/api/taskforce/auth/register')) {
1638
+ currentAccountKey = 'newcomer';
1639
+ return jsonResponse({
1640
+ success: true,
1641
+ workspaceSetupRequired: false,
1642
+ verificationRequired: false
1643
+ });
1644
+ }
1645
+ if (url.includes('/api/taskforce/auth/login')) {
1646
+ const body = typeof init?.body === 'string' ? JSON.parse(init.body) : {};
1647
+ currentAccountKey = String(body?.email || '').trim().toLowerCase() === accounts.ross.email
1648
+ ? 'ross'
1649
+ : 'newcomer';
1650
+ return jsonResponse({ success: true });
1651
+ }
1652
+ if (url.includes('/api/taskforce/account/profile-summary')) {
1653
+ return jsonResponse(account
1654
+ ? {
1655
+ planId: account.planId,
1656
+ planVersionId: account.planVersionId,
1657
+ planName: account.planName,
1658
+ entitlementState: 'active',
1659
+ workspaceId: account.workspaceId,
1660
+ workspaceRole: account.workspaceRole,
1661
+ canManageWorkspace: account.workspaceRole === 'owner' || account.workspaceRole === 'admin',
1662
+ teamPlanMode: account.teamPlanMode,
1663
+ teamManagementAllowed: account.teamManagementAllowed
1664
+ }
1665
+ : {
1666
+ planId: null,
1667
+ planVersionId: null,
1668
+ planName: null,
1669
+ entitlementState: null,
1670
+ workspaceId: 'workspace-local-active',
1671
+ workspaceRole: null,
1672
+ canManageWorkspace: false,
1673
+ teamPlanMode: 'unknown',
1674
+ teamManagementAllowed: false
1675
+ });
1676
+ }
1677
+ if (url.includes('/api/taskforce/auth/workspace-members')) {
1678
+ return jsonResponse({
1679
+ users: account ? [{
1680
+ userId: account.userId,
1681
+ email: account.email,
1682
+ displayName: account.displayName,
1683
+ role: account.workspaceRole,
1684
+ status: 'active',
1685
+ disabled: false
1686
+ }] : []
1687
+ });
1688
+ }
1689
+ if (url.includes('/api/taskforce/public/pricing')) {
1690
+ return jsonResponse({
1691
+ plans: [
1692
+ {
1693
+ planId: 'starter',
1694
+ displayName: 'Starter',
1695
+ status: 'active',
1696
+ defaultVersion: {
1697
+ planVersionId: 'starter-v1',
1698
+ versionNumber: 1,
1699
+ seatLimit: null,
1700
+ metadata: {}
1701
+ },
1702
+ features: [],
1703
+ pricing: {
1704
+ month: { pricingType: 'free', stripePriceId: null, unitAmount: 0, currency: null, interval: 'month' },
1705
+ year: null
1706
+ }
1707
+ },
1708
+ {
1709
+ planId: 'team-5',
1710
+ displayName: 'Team 5',
1711
+ status: 'active',
1712
+ defaultVersion: {
1713
+ planVersionId: 'team-5-v1',
1714
+ versionNumber: 1,
1715
+ seatLimit: 5,
1716
+ metadata: {}
1717
+ },
1718
+ features: [],
1719
+ pricing: {
1720
+ month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 5000, currency: 'usd', interval: 'month' },
1721
+ year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 50000, currency: 'usd', interval: 'year' }
1722
+ }
1723
+ }
1724
+ ]
1725
+ });
1726
+ }
1727
+ if (url.includes('/api/taskforce/ui-state?key=app')) {
1728
+ return jsonResponse({ success: true, state: {} });
1729
+ }
1730
+ if (url.includes('/api/taskforce/ui-state?key=workspace-sync')) {
1731
+ return jsonResponse({
1732
+ success: true,
1733
+ state: {
1734
+ version: 2,
1735
+ enabled: false,
1736
+ phase: 'idle',
1737
+ pullCursor: null,
1738
+ lastPullAt: null,
1739
+ lastPushAt: null,
1740
+ lastSyncedAt: null
1741
+ }
1742
+ });
1743
+ }
1744
+ if (url.includes('/api/taskforce/config')) {
1745
+ return jsonResponse({
1746
+ ...mockConfig,
1747
+ runtimeMode: 'local',
1748
+ authRequiredForApi: false,
1749
+ workspaceId: 'workspace-local-active',
1750
+ setupState: {
1751
+ globalSetupState: 'present',
1752
+ workspaceSetupState: 'present',
1753
+ runtimeMode: 'local',
1754
+ workspaceId: 'workspace-local-active',
1755
+ mode: 'core',
1756
+ forceSetup: false,
1757
+ forceGlobalSetup: false,
1758
+ forceWorkspaceSetup: false
1759
+ }
1760
+ });
1761
+ }
1762
+ if (url.includes('/api/taskforce/tasks')) {
1763
+ return jsonResponse({ tasks: mockTasks });
1764
+ }
1765
+ if (url.includes('/api/taskforce/archive')) {
1766
+ return jsonResponse({ archived: [] });
1767
+ }
1768
+ if (url.includes('/api/taskforce/categories')) {
1769
+ return jsonResponse(mockCategories);
1770
+ }
1771
+ if (url.includes('/api/taskforce/types')) {
1772
+ return jsonResponse({ types: [] });
1773
+ }
1774
+ if (url.includes('/api/taskforce/priorities')) {
1775
+ return jsonResponse({ priorities: [] });
1776
+ }
1777
+ if (url.includes('/api/taskforce/approaches')) {
1778
+ return jsonResponse({ approaches: [] });
1779
+ }
1780
+ if (url.includes('/api/taskforce/specialists')) {
1781
+ return jsonResponse({ specialists: [] });
1782
+ }
1783
+ if (url.includes('/api/taskforce/workflow-templates')) {
1784
+ return jsonResponse({ templates: [] });
1785
+ }
1786
+ if (url.includes('/api/taskforce/workspace/assignee-options')) {
1787
+ return jsonResponse({ assignees: [] });
1788
+ }
1789
+ if (url.includes('/api/taskforce/initiatives')) {
1790
+ return jsonResponse([]);
1791
+ }
1792
+ if (url.includes('/api/taskforce/workstreams')) {
1793
+ return jsonResponse([]);
1794
+ }
1795
+ return jsonResponse({});
1796
+ });
1797
+ vi.stubGlobal('fetch', fetchMock);
1798
+ render(_jsx(TaskforceCore, { config: { cloudAuthBaseUrl: 'https://cloud.example.com' } }));
1799
+ await waitFor(() => {
1800
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1801
+ });
1802
+ await user.click(screen.getByTitle(/Account/i));
1803
+ await waitFor(() => {
1804
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1805
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1806
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1807
+ });
1808
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1809
+ await waitFor(() => {
1810
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1811
+ });
1812
+ await user.click(screen.getByTitle(/Account/i));
1813
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1814
+ await waitFor(() => {
1815
+ expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument();
1816
+ });
1817
+ await user.click(screen.getByRole('button', { name: 'Register' }));
1818
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.newcomer.email);
1819
+ await user.type(screen.getByPlaceholderText('Display name'), accounts.newcomer.displayName);
1820
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1821
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
1822
+ await waitFor(() => {
1823
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1824
+ });
1825
+ await user.click(screen.getByTitle(/Account/i));
1826
+ await waitFor(() => {
1827
+ expect(screen.getByText(accounts.newcomer.email)).toBeInTheDocument();
1828
+ expect(screen.getByText('Starter')).toBeInTheDocument();
1829
+ });
1830
+ await user.click(screen.getByRole('menuitem', { name: 'Sign Out' }));
1831
+ await waitFor(() => {
1832
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1833
+ });
1834
+ await user.click(screen.getByTitle(/Account/i));
1835
+ await user.click(await screen.findByRole('menuitem', { name: 'Sign In / Register' }));
1836
+ await waitFor(() => {
1837
+ expect(screen.getByText('Sign in with your account credentials.')).toBeInTheDocument();
1838
+ });
1839
+ await user.type(await screen.findByPlaceholderText('Email'), accounts.ross.email);
1840
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
1841
+ await user.click(screen.getByRole('button', { name: 'Sign In' }));
1842
+ await waitFor(() => {
1843
+ expect(screen.getByTitle(/Account/i)).toBeInTheDocument();
1844
+ });
1845
+ await user.click(screen.getByTitle(/Account/i));
1846
+ await waitFor(() => {
1847
+ expect(screen.getByText(accounts.ross.email)).toBeInTheDocument();
1848
+ expect(screen.getByText('Team 5')).toBeInTheDocument();
1849
+ expect(screen.queryByText('Starter')).not.toBeInTheDocument();
1850
+ expect(screen.getByRole('menuitem', { name: 'Team Management' })).toBeInTheDocument();
1851
+ });
1852
+ await user.click(screen.getByRole('button', { name: /Subscription Team 5/i }));
1853
+ await waitFor(() => {
1854
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
1855
+ });
1856
+ const currentPlanCard = screen.getByTestId('plan-card-team-5');
1857
+ expect(within(currentPlanCard).getByTestId('plan-manage-team-5')).toHaveTextContent('Current Plan');
1858
+ });
1572
1859
  it('allows local runtime to open the shared cloud-backed register route', async () => {
1573
1860
  const fetchMock = vi.fn((url) => {
1574
1861
  if (url.includes('/api/taskforce/auth/session')) {
@@ -1685,6 +1972,407 @@ describe('TaskforceCore Integration', () => {
1685
1972
  expect(screen.queryByPlaceholderText('Verification token')).not.toBeInTheDocument();
1686
1973
  expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
1687
1974
  });
1975
+ it('routes successful registration into plan selection when onboarding leaves the account pending plan selection', async () => {
1976
+ const user = userEvent.setup();
1977
+ let authenticated = false;
1978
+ const fetchMock = vi.fn((input) => {
1979
+ const url = String(input);
1980
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
1981
+ return jsonResponse({
1982
+ config: {
1983
+ baseUrl: 'https://app.taskforce.example',
1984
+ apiBaseUrl: 'https://app.taskforce.example/api',
1985
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
1986
+ runtimeMode: 'cloud',
1987
+ authSource: 'cloud',
1988
+ workspaceMode: 'multi-cloud',
1989
+ workspaceSwitchingEnabled: true,
1990
+ authRequiredForApi: true
1991
+ }
1992
+ });
1993
+ }
1994
+ if (url.includes('/api/taskforce/auth/session')) {
1995
+ return jsonResponse({
1996
+ authenticated,
1997
+ runtimeMode: 'cloud',
1998
+ authRequiredForApi: true,
1999
+ userId: authenticated ? 'user-1' : null,
2000
+ workspaceId: authenticated ? 'workspace-1' : null,
2001
+ betaAccess: true
2002
+ });
2003
+ }
2004
+ if (url.includes('/api/taskforce/config')) {
2005
+ return jsonResponse({
2006
+ ...mockConfig,
2007
+ runtimeMode: 'cloud',
2008
+ authRequiredForApi: true,
2009
+ workspaceSwitchingEnabled: true,
2010
+ setupState: {
2011
+ globalSetupState: 'present',
2012
+ workspaceSetupState: 'present',
2013
+ runtimeMode: 'cloud',
2014
+ workspaceId: 'workspace-1',
2015
+ mode: 'core',
2016
+ forceSetup: false,
2017
+ forceGlobalSetup: false,
2018
+ forceWorkspaceSetup: false
2019
+ }
2020
+ });
2021
+ }
2022
+ if (url.includes('/api/taskforce/workspaces')) {
2023
+ return jsonResponse({
2024
+ success: true,
2025
+ workspaces: [{
2026
+ id: 'workspace-1',
2027
+ slug: 'workspace-1',
2028
+ name: 'Workspace 1',
2029
+ description: null,
2030
+ role: 'owner',
2031
+ status: 'active',
2032
+ betaAccess: true
2033
+ }],
2034
+ currentWorkspaceId: 'workspace-1'
2035
+ });
2036
+ }
2037
+ if (url.includes('/api/taskforce/account/access-status')) {
2038
+ return jsonResponse({
2039
+ gate: 'plan_selection_required',
2040
+ hasEntitlement: true,
2041
+ canAccessApp: false,
2042
+ canAccessPlans: true,
2043
+ message: 'Choose a plan to continue.'
2044
+ });
2045
+ }
2046
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2047
+ return jsonResponse({
2048
+ entitlementState: 'pending_plan_selection',
2049
+ gate: 'plan_selection_required',
2050
+ message: 'Choose a plan to continue.',
2051
+ workspaceId: 'workspace-1'
2052
+ });
2053
+ }
2054
+ if (url.includes('/api/taskforce/public/pricing')) {
2055
+ return jsonResponse({
2056
+ plans: [{
2057
+ planId: 'starter',
2058
+ displayName: 'Starter',
2059
+ status: 'active',
2060
+ defaultVersion: {
2061
+ planVersionId: 'starter-v1',
2062
+ versionNumber: 1,
2063
+ seatLimit: null,
2064
+ metadata: {}
2065
+ },
2066
+ features: [],
2067
+ pricing: {
2068
+ month: {
2069
+ planVersionId: 'starter-v1',
2070
+ interval: 'month',
2071
+ pricingType: 'free',
2072
+ stripePriceId: 'free:starter-v1:month',
2073
+ displayAmount: '$0.00',
2074
+ unitAmount: 0,
2075
+ currency: 'usd'
2076
+ },
2077
+ year: null
2078
+ }
2079
+ }]
2080
+ });
2081
+ }
2082
+ if (url.includes('/api/taskforce/auth/register')) {
2083
+ authenticated = true;
2084
+ return jsonResponse({
2085
+ success: true,
2086
+ workspaceSetupRequired: false,
2087
+ verificationRequired: false,
2088
+ planSelectionRequired: true,
2089
+ commercialState: 'pending_plan_selection'
2090
+ });
2091
+ }
2092
+ if (url.includes('/api/taskforce/categories')) {
2093
+ return jsonResponse(mockCategories);
2094
+ }
2095
+ if (url.includes('/api/taskforce/tasks')) {
2096
+ return jsonResponse({ tasks: mockTasks });
2097
+ }
2098
+ if (url.includes('/api/taskforce/archive')) {
2099
+ return jsonResponse({ archived: [] });
2100
+ }
2101
+ if (url.includes('/api/taskforce/data-version')) {
2102
+ return jsonResponse({ dataVersion: 1 });
2103
+ }
2104
+ if (url.includes('/api/taskforce/types')) {
2105
+ return jsonResponse({ types: [] });
2106
+ }
2107
+ if (url.includes('/api/taskforce/priorities')) {
2108
+ return jsonResponse({ priorities: [] });
2109
+ }
2110
+ if (url.includes('/api/taskforce/approaches')) {
2111
+ return jsonResponse({ approaches: [] });
2112
+ }
2113
+ if (url.includes('/api/taskforce/specialists')) {
2114
+ return jsonResponse({ specialists: [] });
2115
+ }
2116
+ if (url.includes('/api/taskforce/workflow-templates')) {
2117
+ return jsonResponse({ templates: [] });
2118
+ }
2119
+ return jsonResponse({});
2120
+ });
2121
+ vi.stubGlobal('fetch', fetchMock);
2122
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2123
+ await user.type(await screen.findByPlaceholderText('Email'), 'pending@example.com');
2124
+ await user.type(screen.getByPlaceholderText('Display name'), 'Pending User');
2125
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2126
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2127
+ await waitFor(() => {
2128
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2129
+ expect(screen.getByText('Choose a plan to continue.')).toBeInTheDocument();
2130
+ });
2131
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(false);
2132
+ });
2133
+ it('routes successful registration into checkout when onboarding leaves the account checkout-pending', async () => {
2134
+ const user = userEvent.setup();
2135
+ let authenticated = false;
2136
+ const fetchMock = vi.fn((input) => {
2137
+ const url = String(input);
2138
+ if (url.includes('/api/taskforce/auth/runtime-config')) {
2139
+ return jsonResponse({
2140
+ config: {
2141
+ baseUrl: 'https://app.taskforce.example',
2142
+ apiBaseUrl: 'https://app.taskforce.example/api',
2143
+ cloudAuthBaseUrl: 'https://app.taskforce.example',
2144
+ runtimeMode: 'cloud',
2145
+ authSource: 'cloud',
2146
+ workspaceMode: 'multi-cloud',
2147
+ workspaceSwitchingEnabled: true,
2148
+ authRequiredForApi: true
2149
+ }
2150
+ });
2151
+ }
2152
+ if (url.includes('/api/taskforce/auth/session')) {
2153
+ return jsonResponse({
2154
+ authenticated,
2155
+ runtimeMode: 'cloud',
2156
+ authRequiredForApi: true,
2157
+ userId: authenticated ? 'user-1' : null,
2158
+ workspaceId: authenticated ? 'workspace-1' : null,
2159
+ betaAccess: true
2160
+ });
2161
+ }
2162
+ if (url.includes('/api/taskforce/config')) {
2163
+ return jsonResponse({
2164
+ ...mockConfig,
2165
+ runtimeMode: 'cloud',
2166
+ authRequiredForApi: true,
2167
+ workspaceSwitchingEnabled: true,
2168
+ setupState: {
2169
+ globalSetupState: 'present',
2170
+ workspaceSetupState: 'present',
2171
+ runtimeMode: 'cloud',
2172
+ workspaceId: 'workspace-1',
2173
+ mode: 'core',
2174
+ forceSetup: false,
2175
+ forceGlobalSetup: false,
2176
+ forceWorkspaceSetup: false
2177
+ }
2178
+ });
2179
+ }
2180
+ if (url.includes('/api/taskforce/workspaces')) {
2181
+ return jsonResponse({
2182
+ success: true,
2183
+ workspaces: [{
2184
+ id: 'workspace-1',
2185
+ slug: 'workspace-1',
2186
+ name: 'Workspace 1',
2187
+ description: null,
2188
+ role: 'owner',
2189
+ status: 'active',
2190
+ betaAccess: true
2191
+ }],
2192
+ currentWorkspaceId: 'workspace-1'
2193
+ });
2194
+ }
2195
+ if (url.includes('/api/taskforce/account/access-status')) {
2196
+ return jsonResponse({
2197
+ gate: 'checkout_pending',
2198
+ hasEntitlement: true,
2199
+ canAccessApp: false,
2200
+ canAccessPlans: true,
2201
+ message: 'Complete checkout to continue.'
2202
+ });
2203
+ }
2204
+ if (url.includes('/api/taskforce/account/profile-summary')) {
2205
+ return jsonResponse({
2206
+ planId: 'pro',
2207
+ planName: 'Pro',
2208
+ planVersionId: 'pro-v1',
2209
+ entitlementState: 'checkout_pending',
2210
+ gate: 'checkout_pending',
2211
+ message: 'Complete checkout to continue.',
2212
+ workspaceId: 'workspace-1'
2213
+ });
2214
+ }
2215
+ if (url.includes('/api/taskforce/public/pricing')) {
2216
+ return jsonResponse({
2217
+ plans: [{
2218
+ planId: 'pro',
2219
+ displayName: 'Pro',
2220
+ status: 'active',
2221
+ defaultVersion: {
2222
+ planVersionId: 'pro-v1',
2223
+ versionNumber: 1,
2224
+ seatLimit: null,
2225
+ metadata: {}
2226
+ },
2227
+ features: [],
2228
+ pricing: {
2229
+ month: {
2230
+ planVersionId: 'pro-v1',
2231
+ interval: 'month',
2232
+ pricingType: 'stripe',
2233
+ stripePriceId: 'price_pro_month',
2234
+ displayAmount: '$19.00',
2235
+ unitAmount: 1900,
2236
+ currency: 'usd'
2237
+ },
2238
+ year: {
2239
+ planVersionId: 'pro-v1',
2240
+ interval: 'year',
2241
+ pricingType: 'stripe',
2242
+ stripePriceId: 'price_pro_year',
2243
+ displayAmount: '$190.00',
2244
+ unitAmount: 19000,
2245
+ currency: 'usd'
2246
+ }
2247
+ }
2248
+ }]
2249
+ });
2250
+ }
2251
+ if (url.includes('/api/taskforce/auth/register')) {
2252
+ authenticated = true;
2253
+ return jsonResponse({
2254
+ success: true,
2255
+ workspaceSetupRequired: false,
2256
+ verificationRequired: false,
2257
+ checkoutPending: true,
2258
+ commercialState: 'checkout_pending'
2259
+ });
2260
+ }
2261
+ if (url.includes('/api/taskforce/billing/checkout-session')) {
2262
+ return jsonResponse({
2263
+ checkoutState: 'updated',
2264
+ message: 'Checkout started.'
2265
+ });
2266
+ }
2267
+ if (url.includes('/api/taskforce/categories')) {
2268
+ return jsonResponse(mockCategories);
2269
+ }
2270
+ if (url.includes('/api/taskforce/tasks')) {
2271
+ return jsonResponse({ tasks: mockTasks });
2272
+ }
2273
+ if (url.includes('/api/taskforce/archive')) {
2274
+ return jsonResponse({ archived: [] });
2275
+ }
2276
+ if (url.includes('/api/taskforce/data-version')) {
2277
+ return jsonResponse({ dataVersion: 1 });
2278
+ }
2279
+ if (url.includes('/api/taskforce/types')) {
2280
+ return jsonResponse({ types: [] });
2281
+ }
2282
+ if (url.includes('/api/taskforce/priorities')) {
2283
+ return jsonResponse({ priorities: [] });
2284
+ }
2285
+ if (url.includes('/api/taskforce/approaches')) {
2286
+ return jsonResponse({ approaches: [] });
2287
+ }
2288
+ if (url.includes('/api/taskforce/specialists')) {
2289
+ return jsonResponse({ specialists: [] });
2290
+ }
2291
+ if (url.includes('/api/taskforce/workflow-templates')) {
2292
+ return jsonResponse({ templates: [] });
2293
+ }
2294
+ return jsonResponse({});
2295
+ });
2296
+ vi.stubGlobal('fetch', fetchMock);
2297
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&planId=pro&planVersionId=pro-v1&interval=year&next=%2F'], children: _jsx(TaskforceCore, {}) }));
2298
+ await user.type(await screen.findByPlaceholderText('Email'), 'checkout@example.com');
2299
+ await user.type(screen.getByPlaceholderText('Display name'), 'Checkout User');
2300
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2301
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2302
+ await waitFor(() => {
2303
+ expect(screen.getByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
2304
+ });
2305
+ await waitFor(() => {
2306
+ expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/api/taskforce/billing/checkout-session'))).toBe(true);
2307
+ });
2308
+ });
2309
+ it('shows verification before workspace setup when registration still requires email verification', async () => {
2310
+ const user = userEvent.setup();
2311
+ const fetchMock = vi.fn((input) => {
2312
+ const url = String(input);
2313
+ if (url.includes('/api/taskforce/auth/session')) {
2314
+ return Promise.resolve({
2315
+ ok: true,
2316
+ json: () => Promise.resolve({
2317
+ authenticated: false,
2318
+ runtimeMode: 'cloud',
2319
+ authRequiredForApi: true
2320
+ })
2321
+ });
2322
+ }
2323
+ if (url.includes('/api/taskforce/config')) {
2324
+ return Promise.resolve({
2325
+ ok: true,
2326
+ json: () => Promise.resolve({
2327
+ ...mockConfig,
2328
+ runtimeMode: 'cloud',
2329
+ authRequiredForApi: true,
2330
+ setupState: {
2331
+ globalSetupState: 'present',
2332
+ workspaceSetupState: 'present',
2333
+ runtimeMode: 'cloud',
2334
+ workspaceId: 'default',
2335
+ mode: 'core',
2336
+ forceSetup: false,
2337
+ forceGlobalSetup: false,
2338
+ forceWorkspaceSetup: false
2339
+ }
2340
+ })
2341
+ });
2342
+ }
2343
+ if (url.includes('/api/taskforce/public/pricing')) {
2344
+ return Promise.resolve({
2345
+ ok: true,
2346
+ json: () => Promise.resolve({
2347
+ plans: []
2348
+ })
2349
+ });
2350
+ }
2351
+ if (url.includes('/api/taskforce/auth/register')) {
2352
+ return Promise.resolve({
2353
+ ok: true,
2354
+ json: () => Promise.resolve({
2355
+ success: true,
2356
+ workspaceSetupRequired: true,
2357
+ verificationRequired: true,
2358
+ emailSent: true
2359
+ })
2360
+ });
2361
+ }
2362
+ return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
2363
+ });
2364
+ vi.stubGlobal('fetch', fetchMock);
2365
+ render(_jsx(MemoryRouter, { initialEntries: ['/login?mode=register&next=%2Fplans'], children: _jsx(TaskforceCore, {}) }));
2366
+ await user.type(await screen.findByPlaceholderText('Email'), 'local@example.com');
2367
+ await user.type(screen.getByPlaceholderText('Display name'), 'Local User');
2368
+ await user.type(screen.getByPlaceholderText('Password'), 'Password123!');
2369
+ await user.click(screen.getByRole('button', { name: 'Create Account' }));
2370
+ await waitFor(() => {
2371
+ expect(screen.getByPlaceholderText('Verification token')).toBeInTheDocument();
2372
+ });
2373
+ expect(screen.queryByRole('button', { name: 'Create Account' })).not.toBeInTheDocument();
2374
+ expect(screen.queryByText('Subscription Plans')).not.toBeInTheDocument();
2375
+ });
1688
2376
  it('allows returning to sign-in/register from forgot mode in local runtime', async () => {
1689
2377
  const user = userEvent.setup();
1690
2378
  const fetchMock = vi.fn((url) => {
@@ -1820,7 +2508,7 @@ describe('TaskforceCore Integration', () => {
1820
2508
  json: () => Promise.resolve({
1821
2509
  success: false,
1822
2510
  code: 'SIGNUP_PLAN_NOT_ENABLED',
1823
- error: 'Configured default signup plan version \"team-10-v1\" belongs to a disabled plan.'
2511
+ error: 'Configured onboarding plan version \"team-10-v1\" belongs to a disabled plan.'
1824
2512
  })
1825
2513
  });
1826
2514
  }
@@ -2961,6 +3649,13 @@ describe('TaskforceCore Integration', () => {
2961
3649
  expect(standaloneLayoutSource).toContain('openSharedLoginRoute(\'login\')');
2962
3650
  expect(standaloneLayoutSource).toContain('navigate(`/login?mode=${mode}&next=${encodeURIComponent(target)}`)');
2963
3651
  });
3652
+ it('keeps the account badge neutral until the authenticated identity is fully resolved', () => {
3653
+ const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
3654
+ const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
3655
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountInitial = isAuthenticated && authSessionResolved && accountBadgeLabel.length > 0;");
3656
+ expect(standaloneLayoutSource).toContain("const shouldShowResolvedAccountAvatar = isAuthenticated && authSessionResolved && accountAvatarUrl.length > 0;");
3657
+ expect(standaloneLayoutSource).toContain('<User size={14} className={accountStyles.avatarIcon} />');
3658
+ });
2964
3659
  it('keeps workspace management cloud-only while allowing cloud-backed team management from local runtime', () => {
2965
3660
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
2966
3661
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
@@ -2968,17 +3663,25 @@ describe('TaskforceCore Integration', () => {
2968
3663
  const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
2969
3664
  expect(standaloneLayoutSource).toContain("const canManageCloudWorkspaces = runtimeMode === 'cloud' && workspaceSwitchingEnabled && isAuthenticated;");
2970
3665
  expect(standaloneLayoutSource).toContain("const canAccessCloudTeamManagement = isAuthenticated && (runtimeMode === 'cloud' || cloudAuthConfigured);");
2971
- expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(authWorkspaceId || currentWorkspaceId || '').trim();");
2972
- expect(standaloneLayoutSource).toContain("fetchTaskforceApi('/api/taskforce/account/team-management-access'");
3666
+ expect(standaloneLayoutSource).toContain("const teamManagementWorkspaceId = String(accountProfileSummary?.workspaceId || authWorkspaceId || currentWorkspaceId || '').trim();");
3667
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
2973
3668
  expect(standaloneLayoutSource).toContain('computeCanOpenTeamManagement(canAccessCloudTeamManagement, teamWorkspaceRole, teamPlanMode)');
2974
3669
  expect(useTaskforceSource).toContain("const [authWorkspaceId, setAuthWorkspaceId] = useState<string>('');");
2975
3670
  });
2976
3671
  it('routes local plans and billing UI through the connected cloud billing base', () => {
2977
3672
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');
2978
3673
  const standaloneLayoutSource = fs.readFileSync(standaloneLayoutPath, 'utf8');
2979
- expect(standaloneLayoutSource).toContain("const billingApiBaseUrl = useMemo(() => {");
2980
- expect(standaloneLayoutSource).toContain("loadBillingStatusCached(billingStatusUrl)");
2981
- expect(standaloneLayoutSource).toContain("apiBaseUrl={billingApiBaseUrl}");
3674
+ const plansPagePath = path.join(process.cwd(), 'src/components/views/PlansPage.tsx');
3675
+ const plansPageSource = fs.readFileSync(plansPagePath, 'utf8');
3676
+ const useTaskforcePath = path.join(process.cwd(), 'src/hooks/useTaskforce.ts');
3677
+ const useTaskforceSource = fs.readFileSync(useTaskforcePath, 'utf8');
3678
+ expect(standaloneLayoutSource).toContain("const accountProfileSummaryUrl = resolveCloudAuthUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
3679
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl('/api/taskforce/billing/portal-session')");
3680
+ expect(standaloneLayoutSource).toContain("resolveCloudAuthUrl={resolveCloudAuthUrl}");
3681
+ expect(plansPageSource).toContain('const buildCloudBillingUrl = useCallback((path: string): string => {');
3682
+ expect(plansPageSource).toContain("const accountProfileSummaryUrl = buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH);");
3683
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/account/')");
3684
+ expect(useTaskforceSource).toContain("|| path.startsWith('/api/taskforce/billing/')");
2982
3685
  });
2983
3686
  it('uses shared /login flow for local sign in instead of a duplicate modal', async () => {
2984
3687
  const standaloneLayoutPath = path.join(process.cwd(), 'src/components/views/StandaloneLayout.tsx');