@taskforcehq/taskforce 0.3.304 → 0.3.305
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/README.md +140 -247
- package/dist/TaskforceCore.js +165 -70
- package/dist/TaskforceCore.test.js +799 -90
- package/dist/compat/workspaceSyncCompat.js +6 -0
- package/dist/components/features/DocumentIndex.d.ts +1 -1
- package/dist/components/features/DocumentIndex.js +1 -1
- package/dist/components/features/DocumentViewer.d.ts +1 -1
- package/dist/components/features/DocumentViewer.js +32 -9
- package/dist/components/features/DocumentWorkspace.d.ts +5 -22
- package/dist/components/features/DocumentWorkspace.js +12 -155
- package/dist/components/features/DocumentWorkspace.test.js +193 -6
- package/dist/components/features/TaskSettings.js +29 -3
- package/dist/components/features/TaskSettings.test.js +42 -1
- package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
- package/dist/components/features/documentWorkspaceModel.js +57 -0
- package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
- package/dist/components/features/useDocumentWorkspaceController.js +113 -0
- package/dist/components/task/TaskForm.d.ts +8 -8
- package/dist/components/task/TaskForm.js +358 -221
- package/dist/components/task/TaskForm.test.js +456 -381
- package/dist/components/task/TaskKanban.js +2 -1
- package/dist/components/views/PlanComparisonPage.js +5 -8
- package/dist/components/views/PlanComparisonPage.test.js +51 -41
- package/dist/components/views/PlansPage.js +126 -84
- package/dist/components/views/PlansPage.test.js +308 -39
- package/dist/components/views/StandaloneLayout.js +28 -22
- package/dist/components/views/panels/FilterToolbar.test.js +6 -4
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
- package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
- package/dist/config/envSchema.js +1 -1
- package/dist/core/AiProfileService.d.ts +6 -1
- package/dist/core/AiProfileService.js +161 -16
- package/dist/core/AiProfileService.test.js +3 -1
- package/dist/core/AiProfiles.test.js +200 -0
- package/dist/core/AuthTokenService.test.d.ts +1 -0
- package/dist/core/AuthTokenService.test.js +78 -0
- package/dist/core/EntitlementsPolicy.test.js +75 -13
- package/dist/core/PlanEntitlementService.d.ts +7 -1
- package/dist/core/PlanEntitlementService.js +117 -42
- package/dist/core/PlanVersionPolicy.test.js +19 -26
- package/dist/core/SignupMonetizationSettings.test.js +46 -21
- package/dist/core/SystemAdmin.test.js +212 -56
- package/dist/core/TaskTaxonomyValidation.test.js +53 -0
- package/dist/core/Taskforce.d.ts +3 -0
- package/dist/core/Taskforce.js +50 -10
- package/dist/core/WorkspacePlanMode.test.js +12 -12
- package/dist/core/shared.d.ts +2 -0
- package/dist/core/shared.js +11 -0
- package/dist/hooks/sync/auth.js +3 -1
- package/dist/hooks/sync/transfers.d.ts +2 -1
- package/dist/hooks/sync/transfers.js +8 -4
- package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
- package/dist/hooks/useSyncOrchestrator.js +75 -7
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
- package/dist/hooks/useTaskMutations.d.ts +2 -1
- package/dist/hooks/useTaskMutations.js +12 -1
- package/dist/hooks/useTaskMutations.test.js +30 -1
- package/dist/hooks/useTaskforce.d.ts +5 -2
- package/dist/hooks/useTaskforce.js +17 -5
- package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
- package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
- package/dist/hooks/useWorkspaceSyncController.js +11 -1
- package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
- package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
- package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
- package/dist/mcp/collaborationRegistrar.d.ts +2 -0
- package/dist/mcp/collaborationRegistrar.js +71 -0
- package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
- package/dist/mcp/documentAssetRegistrar.js +346 -0
- package/dist/mcp/runtime.js +2362 -3530
- package/dist/mcp/runtime.test.js +159 -0
- package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
- package/dist/mcp/taskPlanningRegistrar.js +418 -0
- package/dist/mcp/toolCatalog.d.ts +35 -0
- package/dist/mcp/toolCatalog.js +3 -0
- package/dist/server/routes/admin.d.ts +1 -5
- package/dist/server/routes/admin.js +138 -61
- package/dist/server/routes/annotatedAttachments.d.ts +1 -1
- package/dist/server/routes/annotatedAttachments.js +1 -1
- package/dist/server/routes/auth.d.ts +1 -4
- package/dist/server/routes/auth.js +23 -63
- package/dist/server/routes/authSupport.d.ts +30 -0
- package/dist/server/routes/authSupport.js +81 -0
- package/dist/server/routes/billing.d.ts +1 -1
- package/dist/server/routes/billing.js +104 -188
- package/dist/server/routes/billing.test.js +214 -86
- package/dist/server/routes/documentReviews.d.ts +1 -1
- package/dist/server/routes/documentReviews.js +1 -1
- package/dist/server/routes/documents.d.ts +4 -2
- package/dist/server/routes/documents.js +73 -7
- package/dist/server/routes/primitives.d.ts +11 -0
- package/dist/server/routes/primitives.js +73 -0
- package/dist/server/routes/resources.d.ts +1 -1
- package/dist/server/routes/resources.js +1 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/sync.d.ts +1 -1
- package/dist/server/routes/sync.js +1 -1
- package/dist/server/routes/tasks.d.ts +1 -1
- package/dist/server/routes/tasks.js +1 -1
- package/dist/server/routes/workspaces.d.ts +1 -1
- package/dist/server/routes/workspaces.js +1 -1
- package/dist/server/routes.d.ts +2 -9
- package/dist/server/routes.js +76 -162
- package/dist/server/routes.test.js +672 -30
- package/dist/sync/contentSyncState.js +16 -7
- package/dist/sync/syncApplyHandlers.test.js +2 -0
- package/dist/sync/syncService.d.ts +1 -0
- package/dist/sync/syncService.js +8 -0
- package/dist/sync/syncService.test.js +14 -0
- package/dist/sync/workspaceSyncModel.d.ts +3 -0
- package/dist/sync/workspaceSyncModel.js +12 -3
- package/dist/sync/workspaceSyncModel.test.js +34 -0
- package/dist/sync/workspaceSyncState.d.ts +2 -0
- package/dist/sync/workspaceSyncState.js +1 -0
- package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
- package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
- package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
- package/dist/ui/assets/index-DcSI5F86.js +6 -0
- package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
- package/dist/utils/commercialLifecyclePresentation.js +196 -0
- package/package.json +8 -1
- package/scripts/check-cloud-mcp.mjs +83 -0
- package/scripts/playwright-auth.sh +5 -1
- package/scripts/run-billing-performance-smoke.sh +24 -0
- package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
- package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
- package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
- package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
- package/dist/ui/assets/index-DXUdtH1B.js +0 -6
|
@@ -53,7 +53,8 @@ export function TaskKanban({ tasks, columns: kanbanColumns, groupBy, searchQuery
|
|
|
53
53
|
// Sensors
|
|
54
54
|
const sensors = useSensors(useSensor(PointerSensor, {
|
|
55
55
|
activationConstraint: {
|
|
56
|
-
|
|
56
|
+
// Give normal clicks a bit more tolerance so slight trackpad drift still opens the task.
|
|
57
|
+
distance: 10,
|
|
57
58
|
},
|
|
58
59
|
}), useSensor(KeyboardSensor, {
|
|
59
60
|
coordinateGetter: sortableKeyboardCoordinates,
|
|
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
3
3
|
import { CheckCircle2, Loader2 } from 'lucide-react';
|
|
4
4
|
import taskforceStyles from '../../Taskforce.module.css';
|
|
5
5
|
import styles from './PlansPage.module.css';
|
|
6
|
+
import { isCommercialEntitlementActive } from '../../utils/commercialLifecyclePresentation';
|
|
6
7
|
function isDebugModeEnabled() {
|
|
7
8
|
const runtimeOverride = globalThis.__DEBUG_MODE__;
|
|
8
9
|
if (typeof runtimeOverride === 'boolean')
|
|
@@ -60,10 +61,6 @@ function getPlanSortRank(plan) {
|
|
|
60
61
|
return Math.min(...numericAmounts);
|
|
61
62
|
return Number.POSITIVE_INFINITY;
|
|
62
63
|
}
|
|
63
|
-
function isActiveEntitlementState(raw) {
|
|
64
|
-
const value = String(raw || '').trim().toLowerCase();
|
|
65
|
-
return value === 'active' || value === 'trialing' || value === 'grace';
|
|
66
|
-
}
|
|
67
64
|
export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome, footer, isAuthenticated, authSessionResolved = true, currentPlanId, currentPlanVersionId, currentEntitlementState, canManageCurrentPlan = true, pricingEndpoint, statusBanner, onBack, backLabel, backDisabled = false, topActions, shellOwnsScroll = false, showPageBackButton = true, fallbackPricingSource = null, onSelectPlan }) {
|
|
68
65
|
const [plans, setPlans] = useState([]);
|
|
69
66
|
const [pricingSource, setPricingSource] = useState(null);
|
|
@@ -138,11 +135,11 @@ export function PlanComparisonPage({ heading, subtitle, variant = 'app', chrome,
|
|
|
138
135
|
const hasMonth = isSelectablePrice(monthPrice);
|
|
139
136
|
const hasYear = isSelectablePrice(yearPrice);
|
|
140
137
|
const planVersionId = String(plan.defaultVersion?.planVersionId || '').trim();
|
|
141
|
-
const
|
|
138
|
+
const hasActiveEntitlement = isCommercialEntitlementActive(currentEntitlementState);
|
|
139
|
+
const isCurrent = hasActiveEntitlement && (Boolean(planVersionId
|
|
142
140
|
&& currentPlanVersionId
|
|
143
|
-
&& currentPlanVersionId === planVersionId
|
|
144
|
-
&&
|
|
145
|
-
&& Boolean(currentPlanId && currentPlanId === plan.planId));
|
|
141
|
+
&& currentPlanVersionId === planVersionId) || (!currentPlanVersionId
|
|
142
|
+
&& Boolean(currentPlanId && currentPlanId === plan.planId)));
|
|
146
143
|
const defaultInterval = hasMonth ? 'month' : (hasYear ? 'year' : null);
|
|
147
144
|
const defaultPrice = defaultInterval === 'year' ? yearPrice : monthPrice;
|
|
148
145
|
const canSelectCard = Boolean(planVersionId) && (isCurrent ? canManageCurrentPlan : Boolean(defaultInterval));
|
|
@@ -15,11 +15,11 @@ describe('PlanComparisonPage', () => {
|
|
|
15
15
|
},
|
|
16
16
|
plans: [
|
|
17
17
|
{
|
|
18
|
-
planId: '
|
|
19
|
-
displayName: '
|
|
18
|
+
planId: 'free',
|
|
19
|
+
displayName: 'Free',
|
|
20
20
|
status: 'active',
|
|
21
21
|
defaultVersion: {
|
|
22
|
-
planVersionId: '
|
|
22
|
+
planVersionId: 'free-v1',
|
|
23
23
|
versionNumber: 1,
|
|
24
24
|
seatLimit: null,
|
|
25
25
|
metadata: {}
|
|
@@ -31,19 +31,19 @@ describe('PlanComparisonPage', () => {
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
planId: '
|
|
35
|
-
displayName: '
|
|
34
|
+
planId: 'team-5',
|
|
35
|
+
displayName: 'Team 5',
|
|
36
36
|
status: 'active',
|
|
37
37
|
defaultVersion: {
|
|
38
|
-
planVersionId: '
|
|
38
|
+
planVersionId: 'team-5-v2',
|
|
39
39
|
versionNumber: 2,
|
|
40
40
|
seatLimit: 5,
|
|
41
41
|
metadata: {}
|
|
42
42
|
},
|
|
43
43
|
features: [],
|
|
44
44
|
pricing: {
|
|
45
|
-
month: { pricingType: 'stripe', stripePriceId: '
|
|
46
|
-
year: { pricingType: 'stripe', stripePriceId: '
|
|
45
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 2000, currency: 'usd', interval: 'month' },
|
|
46
|
+
year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 20000, currency: 'usd', interval: 'year' }
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
]
|
|
@@ -66,11 +66,11 @@ describe('PlanComparisonPage', () => {
|
|
|
66
66
|
json: async () => ({
|
|
67
67
|
plans: [
|
|
68
68
|
{
|
|
69
|
-
planId: '
|
|
70
|
-
displayName: '
|
|
69
|
+
planId: 'free',
|
|
70
|
+
displayName: 'Free',
|
|
71
71
|
status: 'active',
|
|
72
72
|
defaultVersion: {
|
|
73
|
-
planVersionId: '
|
|
73
|
+
planVersionId: 'free-v1',
|
|
74
74
|
versionNumber: 1,
|
|
75
75
|
seatLimit: null,
|
|
76
76
|
metadata: {}
|
|
@@ -93,7 +93,17 @@ describe('PlanComparisonPage', () => {
|
|
|
93
93
|
expect(screen.queryByText('Pricing source: production (cloud)')).not.toBeInTheDocument();
|
|
94
94
|
});
|
|
95
95
|
it('does not mark a plan current when only the plan id matches but the version does not', async () => {
|
|
96
|
-
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "
|
|
96
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v1", currentEntitlementState: "active", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
97
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
98
|
+
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
it('marks a trialing entitlement as the current plan', async () => {
|
|
101
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: "team-5-v2", currentEntitlementState: "trialing", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
102
|
+
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
103
|
+
expect(screen.getByText('Current Plan')).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
it('does not mark a canceled entitlement as current through the plan-id fallback path', async () => {
|
|
106
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "team-5", currentPlanVersionId: null, currentEntitlementState: "canceled", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
97
107
|
expect(await screen.findByRole('heading', { name: 'Subscription Plans' })).toBeInTheDocument();
|
|
98
108
|
expect(screen.queryByText('Current Plan')).not.toBeInTheDocument();
|
|
99
109
|
});
|
|
@@ -105,16 +115,16 @@ describe('PlanComparisonPage', () => {
|
|
|
105
115
|
const onSelectPlan = vi.fn();
|
|
106
116
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
107
117
|
await screen.findByRole('heading', { name: 'Subscription Plans' });
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
expect(
|
|
111
|
-
expect(within(
|
|
112
|
-
expect(within(
|
|
113
|
-
fireEvent.click(
|
|
118
|
+
const freeHeading = screen.getByRole('heading', { name: 'Free' });
|
|
119
|
+
const freeCard = freeHeading.closest('[role="button"]');
|
|
120
|
+
expect(freeCard).toBeTruthy();
|
|
121
|
+
expect(within(freeCard).getByText('$0.00')).toBeInTheDocument();
|
|
122
|
+
expect(within(freeCard).getByText('/ month')).toBeInTheDocument();
|
|
123
|
+
fireEvent.click(freeCard);
|
|
114
124
|
await waitFor(() => {
|
|
115
125
|
expect(onSelectPlan).toHaveBeenCalledWith({
|
|
116
|
-
planId: '
|
|
117
|
-
planVersionId: '
|
|
126
|
+
planId: 'free',
|
|
127
|
+
planVersionId: 'free-v1',
|
|
118
128
|
interval: 'month',
|
|
119
129
|
pricingType: 'free',
|
|
120
130
|
isCurrentPlan: false
|
|
@@ -123,7 +133,7 @@ describe('PlanComparisonPage', () => {
|
|
|
123
133
|
});
|
|
124
134
|
it('does not trigger current-plan actions when the current plan is not Stripe-managed', async () => {
|
|
125
135
|
const onSelectPlan = vi.fn();
|
|
126
|
-
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "
|
|
136
|
+
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: true, currentPlanId: "free", currentPlanVersionId: "free-v1", currentEntitlementState: "active", canManageCurrentPlan: false, onBack: () => { }, backLabel: "Back", onSelectPlan: onSelectPlan }));
|
|
127
137
|
await screen.findByRole('heading', { name: 'Subscription Plans' });
|
|
128
138
|
const currentButton = screen.getByRole('button', { name: 'Current Plan' });
|
|
129
139
|
expect(currentButton).toBeDisabled();
|
|
@@ -139,11 +149,11 @@ describe('PlanComparisonPage', () => {
|
|
|
139
149
|
},
|
|
140
150
|
plans: [
|
|
141
151
|
{
|
|
142
|
-
planId: '
|
|
143
|
-
displayName: '
|
|
152
|
+
planId: 'free',
|
|
153
|
+
displayName: 'Free',
|
|
144
154
|
status: 'active',
|
|
145
155
|
defaultVersion: {
|
|
146
|
-
planVersionId: '
|
|
156
|
+
planVersionId: 'free-v1',
|
|
147
157
|
versionNumber: 1,
|
|
148
158
|
seatLimit: null,
|
|
149
159
|
metadata: {}
|
|
@@ -159,7 +169,7 @@ describe('PlanComparisonPage', () => {
|
|
|
159
169
|
});
|
|
160
170
|
vi.stubGlobal('fetch', fetchMock);
|
|
161
171
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, pricingEndpoint: "https://performance-app.example/api/taskforce/public/pricing", onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
162
|
-
expect(await screen.findByRole('heading', { name: '
|
|
172
|
+
expect(await screen.findByRole('heading', { name: 'Free' })).toBeInTheDocument();
|
|
163
173
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
164
174
|
expect(fetchMock).toHaveBeenCalledWith('https://performance-app.example/api/taskforce/public/pricing', {
|
|
165
175
|
method: 'GET',
|
|
@@ -177,27 +187,27 @@ describe('PlanComparisonPage', () => {
|
|
|
177
187
|
},
|
|
178
188
|
plans: [
|
|
179
189
|
{
|
|
180
|
-
planId: '
|
|
181
|
-
displayName: '
|
|
190
|
+
planId: 'team-5',
|
|
191
|
+
displayName: 'Team 5',
|
|
182
192
|
status: 'active',
|
|
183
193
|
defaultVersion: {
|
|
184
|
-
planVersionId: '
|
|
194
|
+
planVersionId: 'team-5-v2',
|
|
185
195
|
versionNumber: 3,
|
|
186
|
-
seatLimit:
|
|
196
|
+
seatLimit: 5,
|
|
187
197
|
metadata: {}
|
|
188
198
|
},
|
|
189
199
|
features: [],
|
|
190
200
|
pricing: {
|
|
191
|
-
month: { pricingType: 'stripe', stripePriceId: '
|
|
192
|
-
year: { pricingType: 'stripe', stripePriceId: '
|
|
201
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_team_5_month', unitAmount: 12000, currency: 'usd', interval: 'month' },
|
|
202
|
+
year: { pricingType: 'stripe', stripePriceId: 'price_team_5_year', unitAmount: 120000, currency: 'usd', interval: 'year' }
|
|
193
203
|
}
|
|
194
204
|
},
|
|
195
205
|
{
|
|
196
|
-
planId: '
|
|
197
|
-
displayName: '
|
|
206
|
+
planId: 'free',
|
|
207
|
+
displayName: 'Free',
|
|
198
208
|
status: 'active',
|
|
199
209
|
defaultVersion: {
|
|
200
|
-
planVersionId: '
|
|
210
|
+
planVersionId: 'free-v1',
|
|
201
211
|
versionNumber: 1,
|
|
202
212
|
seatLimit: null,
|
|
203
213
|
metadata: {}
|
|
@@ -209,19 +219,19 @@ describe('PlanComparisonPage', () => {
|
|
|
209
219
|
}
|
|
210
220
|
},
|
|
211
221
|
{
|
|
212
|
-
planId: '
|
|
213
|
-
displayName: '
|
|
222
|
+
planId: 'basic',
|
|
223
|
+
displayName: 'Basic',
|
|
214
224
|
status: 'active',
|
|
215
225
|
defaultVersion: {
|
|
216
|
-
planVersionId: '
|
|
226
|
+
planVersionId: 'basic-v1',
|
|
217
227
|
versionNumber: 2,
|
|
218
|
-
seatLimit:
|
|
228
|
+
seatLimit: 1,
|
|
219
229
|
metadata: {}
|
|
220
230
|
},
|
|
221
231
|
features: [],
|
|
222
232
|
pricing: {
|
|
223
|
-
month: { pricingType: 'stripe', stripePriceId: '
|
|
224
|
-
year: { pricingType: 'stripe', stripePriceId: '
|
|
233
|
+
month: { pricingType: 'stripe', stripePriceId: 'price_basic_month', unitAmount: 3000, currency: 'usd', interval: 'month' },
|
|
234
|
+
year: { pricingType: 'stripe', stripePriceId: 'price_basic_year', unitAmount: 30000, currency: 'usd', interval: 'year' }
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
]
|
|
@@ -231,7 +241,7 @@ describe('PlanComparisonPage', () => {
|
|
|
231
241
|
render(_jsx(PlanComparisonPage, { heading: "Subscription Plans", isAuthenticated: false, onBack: () => { }, backLabel: "Back", onSelectPlan: () => { } }));
|
|
232
242
|
await screen.findByRole('heading', { name: 'Subscription Plans' });
|
|
233
243
|
const planNames = screen.getAllByRole('heading', { level: 3 }).map((node) => node.textContent);
|
|
234
|
-
expect(planNames).toEqual(['
|
|
244
|
+
expect(planNames).toEqual(['Free', 'Basic', 'Team 5']);
|
|
235
245
|
});
|
|
236
246
|
it('defaults to the same-origin pricing endpoint', async () => {
|
|
237
247
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
@@ -7,18 +7,15 @@ import { PlanComparisonPage } from './PlanComparisonPage';
|
|
|
7
7
|
import { AppShellHeader } from './AppShellHeader';
|
|
8
8
|
import { resolveTaskforceCloudEnvironmentFromBaseUrl } from '../../config/cloudEnvironment';
|
|
9
9
|
import { ACCOUNT_PROFILE_SUMMARY_PATH, invalidateAccountProfileSummaryCache, loadAccountProfileSummaryCached } from '../../utils/accountProfileSummaryCache';
|
|
10
|
+
import { isCommercialEntitlementActive, resolveCommercialLifecyclePresentation } from '../../utils/commercialLifecyclePresentation';
|
|
10
11
|
const TRANSIENT_CHECKOUT_QUERY_CLEAR_DELAY_MS = 1600;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
};
|
|
12
|
+
const CHECKOUT_CONFIRM_RETRY_DELAYS_MS = [0, 500, 1000, 2000];
|
|
13
|
+
function shouldRetryCheckoutConfirmation(statusCode, errorCode) {
|
|
14
|
+
if (statusCode === 401) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
return String(errorCode || '').trim().toUpperCase() === 'CHECKOUT_SESSION_INCOMPLETE';
|
|
18
|
+
}
|
|
22
19
|
function parseInterval(raw) {
|
|
23
20
|
return raw === 'year' ? 'year' : 'month';
|
|
24
21
|
}
|
|
@@ -45,6 +42,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
45
42
|
const [actionBusy, setActionBusy] = useState(false);
|
|
46
43
|
const [feedback, setFeedback] = useState(null);
|
|
47
44
|
const checkoutStartRan = useRef(false);
|
|
45
|
+
const checkoutSuccessHandledRef = useRef(null);
|
|
48
46
|
const transientQueryClearTimeoutRef = useRef(null);
|
|
49
47
|
const query = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
|
50
48
|
const checkoutState = String(query.get('checkout') || '').trim().toLowerCase();
|
|
@@ -89,32 +87,9 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
89
87
|
].map((value) => String(value || '').trim()).filter((value, index, all) => value && all.indexOf(value) === index);
|
|
90
88
|
return endpoints;
|
|
91
89
|
}, [normalizedApiBaseUrl]);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}, [accountProfileSummary?.entitlementState]);
|
|
96
|
-
const gateMessage = useMemo(() => {
|
|
97
|
-
if (hasResolvedActiveEntitlement)
|
|
98
|
-
return null;
|
|
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')
|
|
109
|
-
return GATE_MESSAGES.planSelectionRequired;
|
|
110
|
-
if (effectiveGateState === 'checkout_pending')
|
|
111
|
-
return GATE_MESSAGES.checkoutPending;
|
|
112
|
-
if (effectiveGateState === 'misconfigured')
|
|
113
|
-
return GATE_MESSAGES.misconfigured;
|
|
114
|
-
if (effectiveGateState === 'missing_entitlement')
|
|
115
|
-
return GATE_MESSAGES.missingEntitlement;
|
|
116
|
-
return null;
|
|
117
|
-
}, [accountProfileSummary?.billingConflictCode, accountProfileSummary?.gate, accountProfileSummary?.message, gateState, hasResolvedActiveEntitlement]);
|
|
90
|
+
const lifecyclePresentation = useMemo(() => resolveCommercialLifecyclePresentation(accountProfileSummary, gateState), [accountProfileSummary, gateState]);
|
|
91
|
+
const hasResolvedActiveEntitlement = useMemo(() => lifecyclePresentation.allowReturnToApp, [lifecyclePresentation.allowReturnToApp]);
|
|
92
|
+
const gateMessage = useMemo(() => lifecyclePresentation.message, [lifecyclePresentation.message]);
|
|
118
93
|
const clearTransientQueryState = useCallback(() => {
|
|
119
94
|
const next = new URLSearchParams(location.search);
|
|
120
95
|
next.delete('checkout');
|
|
@@ -157,30 +132,19 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
157
132
|
}
|
|
158
133
|
}, [authUserId, buildCloudBillingUrl, isAuthenticated, onAccountProfileSummaryChange]);
|
|
159
134
|
const resolveRecoveryMessage = useCallback((summary, fallback) => {
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
return
|
|
163
|
-
if (billingConflictCode === 'CHECKOUT_SESSION_SUPERSEDED')
|
|
164
|
-
return BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED;
|
|
165
|
-
const summaryMessage = String(summary?.message || '').trim();
|
|
166
|
-
if (summaryMessage)
|
|
167
|
-
return summaryMessage;
|
|
135
|
+
const message = resolveCommercialLifecyclePresentation(summary, gateState).message;
|
|
136
|
+
if (message)
|
|
137
|
+
return message;
|
|
168
138
|
return String(fallback || 'Unable to confirm checkout completion.').trim() || 'Unable to confirm checkout completion.';
|
|
139
|
+
}, [gateState]);
|
|
140
|
+
const shouldAutoContinueAfterActivation = useCallback((summary, fallbackGate) => {
|
|
141
|
+
return !resolveCommercialLifecyclePresentation(summary, fallbackGate).allowReturnToApp;
|
|
169
142
|
}, []);
|
|
170
|
-
const
|
|
171
|
-
const normalized = String(message || '').trim();
|
|
172
|
-
if (normalized === GATE_MESSAGES.planSelectionRequired
|
|
173
|
-
|| normalized === GATE_MESSAGES.checkoutPending) {
|
|
174
|
-
return 'info';
|
|
175
|
-
}
|
|
176
|
-
return 'error';
|
|
177
|
-
}, []);
|
|
178
|
-
const startCheckout = useCallback(async (planVersionId, interval) => {
|
|
143
|
+
const startCheckout = useCallback(async (planVersionId, interval, options) => {
|
|
179
144
|
if (!planVersionId) {
|
|
180
145
|
setFeedback({ type: 'error', message: 'Selected plan is missing a default plan version.' });
|
|
181
146
|
return;
|
|
182
147
|
}
|
|
183
|
-
const hadActiveEntitlement = hasResolvedActiveEntitlement;
|
|
184
148
|
setActionBusy(true);
|
|
185
149
|
setFeedback(null);
|
|
186
150
|
try {
|
|
@@ -201,7 +165,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
201
165
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
202
166
|
const summary = await loadAccountProfileSummary({ force: true });
|
|
203
167
|
const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
|
|
204
|
-
if (
|
|
168
|
+
if (options?.autoContinueOnActivation === true && isCommercialEntitlementActive(summaryState)) {
|
|
205
169
|
await onContinueToTaskforce?.();
|
|
206
170
|
}
|
|
207
171
|
return;
|
|
@@ -211,7 +175,7 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
211
175
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
212
176
|
const summary = await loadAccountProfileSummary({ force: true });
|
|
213
177
|
const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
|
|
214
|
-
if (
|
|
178
|
+
if (options?.autoContinueOnActivation === true && isCommercialEntitlementActive(summaryState)) {
|
|
215
179
|
await onContinueToTaskforce?.();
|
|
216
180
|
}
|
|
217
181
|
return;
|
|
@@ -253,12 +217,13 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
253
217
|
finally {
|
|
254
218
|
setActionBusy(false);
|
|
255
219
|
}
|
|
256
|
-
}, [authUserId, buildCloudBillingUrl,
|
|
257
|
-
const confirmCheckoutSession = useCallback(async (sessionId) => {
|
|
220
|
+
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, onContinueToTaskforce, runtimeMode]);
|
|
221
|
+
const confirmCheckoutSession = useCallback(async (sessionId, options) => {
|
|
258
222
|
if (!sessionId) {
|
|
259
223
|
return {
|
|
260
224
|
confirmed: false,
|
|
261
|
-
errorMessage: 'Checkout session is missing.'
|
|
225
|
+
errorMessage: 'Checkout session is missing.',
|
|
226
|
+
retryable: false
|
|
262
227
|
};
|
|
263
228
|
}
|
|
264
229
|
try {
|
|
@@ -270,15 +235,17 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
270
235
|
});
|
|
271
236
|
const payload = await res.json().catch(() => ({}));
|
|
272
237
|
if (!res.ok) {
|
|
238
|
+
const errorCode = String(payload?.code || '').trim();
|
|
273
239
|
return {
|
|
274
240
|
confirmed: false,
|
|
275
|
-
errorMessage: String(payload?.error || 'Unable to confirm checkout completion.')
|
|
241
|
+
errorMessage: String(payload?.error || 'Unable to confirm checkout completion.'),
|
|
242
|
+
retryable: shouldRetryCheckoutConfirmation(res.status, errorCode)
|
|
276
243
|
};
|
|
277
244
|
}
|
|
278
245
|
invalidateAccountProfileSummaryCache(buildCloudBillingUrl(ACCOUNT_PROFILE_SUMMARY_PATH), { identityKey: authUserId });
|
|
279
246
|
const summary = await loadAccountProfileSummary({ force: true });
|
|
280
247
|
const summaryState = String(summary?.entitlementState || '').trim().toLowerCase();
|
|
281
|
-
if (
|
|
248
|
+
if (options?.autoContinueOnActivation === true && isCommercialEntitlementActive(summaryState)) {
|
|
282
249
|
await onContinueToTaskforce?.();
|
|
283
250
|
}
|
|
284
251
|
return { confirmed: true };
|
|
@@ -286,7 +253,8 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
286
253
|
catch {
|
|
287
254
|
return {
|
|
288
255
|
confirmed: false,
|
|
289
|
-
errorMessage: 'Unable to confirm checkout completion.'
|
|
256
|
+
errorMessage: 'Unable to confirm checkout completion.',
|
|
257
|
+
retryable: true
|
|
290
258
|
};
|
|
291
259
|
}
|
|
292
260
|
}, [authUserId, buildCloudBillingUrl, loadAccountProfileSummary, onContinueToTaskforce]);
|
|
@@ -337,15 +305,84 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
337
305
|
}
|
|
338
306
|
}, []);
|
|
339
307
|
useEffect(() => {
|
|
308
|
+
if (checkoutState !== 'success') {
|
|
309
|
+
checkoutSuccessHandledRef.current = null;
|
|
310
|
+
}
|
|
311
|
+
}, [checkoutState]);
|
|
312
|
+
useEffect(() => {
|
|
313
|
+
let cancelled = false;
|
|
314
|
+
const pendingRetryTimeouts = new Set();
|
|
315
|
+
const waitForRetry = async (delayMs) => {
|
|
316
|
+
if (delayMs <= 0)
|
|
317
|
+
return;
|
|
318
|
+
await new Promise((resolve) => {
|
|
319
|
+
const timeoutId = window.setTimeout(() => {
|
|
320
|
+
pendingRetryTimeouts.delete(timeoutId);
|
|
321
|
+
resolve();
|
|
322
|
+
}, delayMs);
|
|
323
|
+
pendingRetryTimeouts.add(timeoutId);
|
|
324
|
+
});
|
|
325
|
+
};
|
|
340
326
|
if (checkoutState === 'success') {
|
|
341
327
|
checkoutStartRan.current = true;
|
|
342
328
|
setFeedback({ type: 'success', message: 'Checkout completed successfully. Finalizing your plan...' });
|
|
329
|
+
if (!authSessionResolved) {
|
|
330
|
+
return () => {
|
|
331
|
+
cancelled = true;
|
|
332
|
+
pendingRetryTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
|
333
|
+
pendingRetryTimeouts.clear();
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (!isAuthenticated) {
|
|
337
|
+
setFeedback({ type: 'error', message: 'Sign in to finish activating your plan.' });
|
|
338
|
+
return () => {
|
|
339
|
+
cancelled = true;
|
|
340
|
+
pendingRetryTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
|
341
|
+
pendingRetryTimeouts.clear();
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const successFlowKey = querySessionId || '__missing_session__';
|
|
345
|
+
if (checkoutSuccessHandledRef.current === successFlowKey) {
|
|
346
|
+
return () => {
|
|
347
|
+
cancelled = true;
|
|
348
|
+
pendingRetryTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
|
349
|
+
pendingRetryTimeouts.clear();
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
checkoutSuccessHandledRef.current = successFlowKey;
|
|
343
353
|
void (async () => {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
const preConfirmationSummary = await loadAccountProfileSummary({ force: true });
|
|
355
|
+
if (cancelled)
|
|
356
|
+
return;
|
|
357
|
+
let confirmation = querySessionId
|
|
358
|
+
? { confirmed: false, errorMessage: 'Unable to confirm checkout completion.', retryable: true }
|
|
359
|
+
: { confirmed: false, errorMessage: 'Checkout session is missing.', retryable: false };
|
|
360
|
+
for (const delayMs of CHECKOUT_CONFIRM_RETRY_DELAYS_MS) {
|
|
361
|
+
await waitForRetry(delayMs);
|
|
362
|
+
if (cancelled)
|
|
363
|
+
return;
|
|
364
|
+
confirmation = querySessionId
|
|
365
|
+
? await confirmCheckoutSession(querySessionId, {
|
|
366
|
+
autoContinueOnActivation: shouldAutoContinueAfterActivation(preConfirmationSummary, gateState)
|
|
367
|
+
})
|
|
368
|
+
: { confirmed: false, errorMessage: 'Checkout session is missing.', retryable: false };
|
|
369
|
+
if (cancelled)
|
|
370
|
+
return;
|
|
371
|
+
if (confirmation.confirmed) {
|
|
372
|
+
setFeedback({ type: 'success', message: 'Checkout completed successfully.' });
|
|
373
|
+
scheduleTransientQueryClear();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!confirmation.retryable) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (cancelled)
|
|
381
|
+
return;
|
|
347
382
|
if (!confirmation.confirmed) {
|
|
348
383
|
const summary = await loadAccountProfileSummary({ force: true });
|
|
384
|
+
if (cancelled)
|
|
385
|
+
return;
|
|
349
386
|
setFeedback({
|
|
350
387
|
type: 'error',
|
|
351
388
|
message: resolveRecoveryMessage(summary, confirmation.errorMessage)
|
|
@@ -353,10 +390,12 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
353
390
|
scheduleTransientQueryClear();
|
|
354
391
|
return;
|
|
355
392
|
}
|
|
356
|
-
setFeedback({ type: 'success', message: 'Checkout completed successfully.' });
|
|
357
|
-
scheduleTransientQueryClear();
|
|
358
393
|
})();
|
|
359
|
-
return
|
|
394
|
+
return () => {
|
|
395
|
+
cancelled = true;
|
|
396
|
+
pendingRetryTimeouts.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
|
397
|
+
pendingRetryTimeouts.clear();
|
|
398
|
+
};
|
|
360
399
|
}
|
|
361
400
|
if (checkoutState === 'cancel') {
|
|
362
401
|
void (async () => {
|
|
@@ -371,29 +410,31 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
371
410
|
}
|
|
372
411
|
if ((checkoutState === 'start' || checkoutState === 'free') && isAuthenticated && queryPlanVersionId && !checkoutStartRan.current) {
|
|
373
412
|
checkoutStartRan.current = true;
|
|
374
|
-
void
|
|
413
|
+
void (async () => {
|
|
414
|
+
const preCheckoutSummary = await loadAccountProfileSummary({ force: true });
|
|
415
|
+
await startCheckout(queryPlanVersionId, queryInterval, {
|
|
416
|
+
autoContinueOnActivation: shouldAutoContinueAfterActivation(preCheckoutSummary, gateState)
|
|
417
|
+
});
|
|
418
|
+
})();
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
}, [authSessionResolved, checkoutState, confirmCheckoutSession, gateState, isAuthenticated, loadAccountProfileSummary, queryInterval, queryPlanVersionId, querySessionId, resolveRecoveryMessage, scheduleTransientQueryClear, shouldAutoContinueAfterActivation, startCheckout]);
|
|
422
|
+
useEffect(() => {
|
|
423
|
+
if (checkoutState === 'success' || checkoutState === 'cancel' || checkoutState === 'start' || checkoutState === 'free') {
|
|
375
424
|
return;
|
|
376
425
|
}
|
|
377
426
|
if (gateMessage) {
|
|
378
|
-
setFeedback({ type:
|
|
427
|
+
setFeedback({ type: lifecyclePresentation.statusTone, message: gateMessage });
|
|
379
428
|
return;
|
|
380
429
|
}
|
|
381
430
|
setFeedback((current) => {
|
|
382
431
|
if (!current || current.type !== 'error')
|
|
383
432
|
return current;
|
|
384
|
-
if (current.message ===
|
|
385
|
-
return null;
|
|
386
|
-
if (current.message === GATE_MESSAGES.misconfigured)
|
|
387
|
-
return null;
|
|
388
|
-
if (current.message === GATE_MESSAGES.missingEntitlement)
|
|
389
|
-
return null;
|
|
390
|
-
if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_EXPIRED)
|
|
391
|
-
return null;
|
|
392
|
-
if (current.message === BILLING_CONFLICT_MESSAGES.CHECKOUT_SESSION_SUPERSEDED)
|
|
433
|
+
if (current.message === lifecyclePresentation.message)
|
|
393
434
|
return null;
|
|
394
435
|
return current;
|
|
395
436
|
});
|
|
396
|
-
}, [checkoutState,
|
|
437
|
+
}, [checkoutState, gateMessage, lifecyclePresentation.message, lifecyclePresentation.statusTone]);
|
|
397
438
|
const handleSelectPlan = useCallback(async (intent) => {
|
|
398
439
|
if (!isAuthenticated) {
|
|
399
440
|
const nextCheckoutState = intent.pricingType === 'free' ? 'free' : 'start';
|
|
@@ -414,12 +455,13 @@ export function PlansPage({ isAuthenticated, authUserId, runtimeMode, authSessio
|
|
|
414
455
|
await openBillingPortal();
|
|
415
456
|
return;
|
|
416
457
|
}
|
|
458
|
+
const autoContinueOnActivation = shouldAutoContinueAfterActivation(accountProfileSummary, gateState);
|
|
417
459
|
if (intent.pricingType === 'free') {
|
|
418
|
-
await startCheckout(intent.planVersionId, intent.interval);
|
|
460
|
+
await startCheckout(intent.planVersionId, intent.interval, { autoContinueOnActivation });
|
|
419
461
|
return;
|
|
420
462
|
}
|
|
421
|
-
await startCheckout(intent.planVersionId, intent.interval);
|
|
422
|
-
}, [isAuthenticated, navigate, openBillingPortal, startCheckout]);
|
|
463
|
+
await startCheckout(intent.planVersionId, intent.interval, { autoContinueOnActivation });
|
|
464
|
+
}, [accountProfileSummary, gateState, isAuthenticated, navigate, openBillingPortal, shouldAutoContinueAfterActivation, startCheckout]);
|
|
423
465
|
const handleBack = useCallback(() => {
|
|
424
466
|
if (hasResolvedActiveEntitlement && onContinueToTaskforce) {
|
|
425
467
|
void onContinueToTaskforce();
|