diff --git a/api/services/enterprise/rbac_service.py b/api/services/enterprise/rbac_service.py index 86cf4c210bd..5e318f3ff18 100644 --- a/api/services/enterprise/rbac_service.py +++ b/api/services/enterprise/rbac_service.py @@ -317,9 +317,6 @@ _LEGACY_WORKSPACE_OWNER_KEYS: list[str] = [ "credential.use", "credential.create", "credential.manage", - "billing.view", - "billing.subscription.manage", - "billing.manage", "app.acl.preview", "app_library.access", "app.create_and_management", @@ -349,9 +346,6 @@ _LEGACY_WORKSPACE_ADMIN_KEYS: list[str] = [ "credential.use", "credential.create", "credential.manage", - "billing.view", - "billing.subscription.manage", - "billing.manage", "app_library.access", "app.create_and_management", "app.tag.manage", @@ -377,9 +371,6 @@ _LEGACY_WORKSPACE_EDITOR_KEYS: list[str] = [ "dataset.external.connect", "snippets.create_and_modify", "tool.manage", - "billing.view", - "billing.subscription.manage", - "billing.manage", ] _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [ @@ -387,9 +378,6 @@ _LEGACY_WORKSPACE_NORMAL_KEYS: list[str] = [ "plugin.install", "credential.use", "app_library.access", - "billing.view", - "billing.subscription.manage", - "billing.manage", ] _LEGACY_WORKSPACE_DATASET_OPERATOR_KEYS: list[str] = [ @@ -805,7 +793,6 @@ class RBACService: data = _inner_call( "GET", f"{_INNER_PREFIX}/role-permissions/catalog", - params={"billing_enabled": dify_config.BILLING_ENABLED}, tenant_id=tenant_id, account_id=account_id, ) diff --git a/api/tests/unit_tests/services/enterprise/test_rbac_service.py b/api/tests/unit_tests/services/enterprise/test_rbac_service.py index 85638b11fff..4c8b779491c 100644 --- a/api/tests/unit_tests/services/enterprise/test_rbac_service.py +++ b/api/tests/unit_tests/services/enterprise/test_rbac_service.py @@ -46,7 +46,7 @@ class TestCatalog: assert call.tenant_id == "tenant-1" assert call.account_id == "acct-1" assert call.json is None - assert call.params == {"billing_enabled": svc.dify_config.BILLING_ENABLED} + assert call.params is None assert len(out.groups) == 1 assert out.groups[0].group_key == "workspace" @@ -624,12 +624,12 @@ class TestMyPermissions: assert out.app.overrides == [] assert out.dataset.overrides == [] if role == "owner": - assert "billing.view" in out.workspace.permission_keys assert "snippets.management" in out.workspace.permission_keys assert "app.acl.preview" in out.workspace.permission_keys assert "dataset.acl.preview" in out.workspace.permission_keys assert "app.acl.preview" in out.app.default_permission_keys assert "dataset.acl.preview" in out.dataset.default_permission_keys + assert not any(key.startswith("billing.") for key in out.workspace.permission_keys) if role == "editor": assert "app.acl.log_and_annotation" in out.app.default_permission_keys diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 22ebdd420a2..d43b873a62a 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -41,10 +41,6 @@ vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) }) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/version-state', async () => { const { createVersionStateModuleMock } = await import('@/test/console/state-fixture') return createVersionStateModuleMock(() => mockConsoleState) @@ -143,7 +139,7 @@ const setupProviderContext = ( const setupConsoleState = (overrides: Record = {}) => { mockConsoleState = { isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.view', 'billing.manage', 'billing.subscription.manage'], + workspacePermissionKeys: [], userProfile: { email: 'test@example.com' }, langGeniusVersionInfo: { current_version: '1.0.0' }, ...overrides, @@ -245,11 +241,11 @@ describe('Billing Page + Plan Integration', () => { // Verify billing URL button visibility and behavior describe('Billing URL button', () => { - it('should show billing button when manager has subscription management permission', () => { + it('should show billing button to managers without billing permission keys', () => { setupProviderContext({ type: Plan.sandbox }) setupConsoleState({ isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.subscription.manage'], + workspacePermissionKeys: [], }) render() @@ -258,11 +254,10 @@ describe('Billing Page + Plan Integration', () => { expect(screen.getByText(/viewBillingAction/i)).toBeInTheDocument() }) - it('should hide billing button when subscription management permission is granted without manager role', () => { + it('should hide billing button from non-manager members', () => { setupProviderContext({ type: Plan.sandbox }) setupConsoleState({ isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.subscription.manage'], }) render() @@ -270,16 +265,16 @@ describe('Billing Page + Plan Integration', () => { expect(screen.queryByText(/viewBillingTitle/i)).not.toBeInTheDocument() }) - it('should hide billing button when subscription management permission is missing', () => { + it('should show billing button when a manager has no billing permission keys', () => { setupProviderContext({ type: Plan.sandbox }) setupConsoleState({ isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.view', 'billing.manage'], + workspacePermissionKeys: [], }) render() - expect(screen.queryByText(/viewBillingTitle/i)).not.toBeInTheDocument() + expect(screen.getByText(/viewBillingTitle/i)).toBeInTheDocument() }) it('should hide billing button when billing is disabled', () => { @@ -289,17 +284,6 @@ describe('Billing Page + Plan Integration', () => { expect(screen.queryByText(/viewBillingTitle/i)).not.toBeInTheDocument() }) - - it('should hide billing button when no billing permissions are granted', () => { - setupProviderContext({ type: Plan.sandbox }) - setupConsoleState({ - workspacePermissionKeys: [], - }) - - render() - - expect(screen.queryByText(/viewBillingTitle/i)).not.toBeInTheDocument() - }) }) }) @@ -366,6 +350,39 @@ describe('Plan Type Display Integration', () => { expect(screen.getByText(/toVerified/i)).toBeInTheDocument() }) + + it('should show education discount to managers without billing permission keys', () => { + setupProviderContext( + { type: Plan.sandbox }, + { + enableEducationPlan: true, + isEducationAccount: true, + }, + ) + setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [] }) + + render() + + expect(screen.getByText(/useEducationDiscount/i)).toBeInTheDocument() + }) + + it('should hide education discount from non-manager members', () => { + setupProviderContext( + { type: Plan.sandbox }, + { + enableEducationPlan: true, + isEducationAccount: true, + }, + ) + setupConsoleState({ + isCurrentWorkspaceManager: false, + workspacePermissionKeys: ['billing.manage'], + }) + + render() + + expect(screen.queryByText(/useEducationDiscount/i)).not.toBeInTheDocument() + }) }) // ═══════════════════════════════════════════════════════════════════════════ diff --git a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx index fbe9d8f987d..f396a6c0c0a 100644 --- a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx +++ b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx @@ -2,7 +2,7 @@ * Integration test: Cloud Plan Payment Flow * * Tests the payment flow for cloud plan items: - * CloudPlanItem → Button click → permission check → fetch URL → redirect + * CloudPlanItem → Button click → payment capability check → fetch URL → redirect * * Covers plan comparison, downgrade prevention, monthly/yearly pricing, * and workspace manager permission enforcement. @@ -30,10 +30,6 @@ vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) }) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => mockConsoleState) -}) // ─── Service mocks ─────────────────────────────────────────────────────────── vi.mock('@/service/billing', () => ({ @@ -65,7 +61,6 @@ vi.mock('@/next/navigation', () => ({ const setupConsoleState = (overrides: Record = {}) => { mockConsoleState = { isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.view', 'billing.manage', 'billing.subscription.manage'], ...overrides, } } @@ -275,15 +270,11 @@ describe('Cloud Plan Payment Flow', () => { }) }) - // ─── 5. Permission Check ──────────────────────────────────────────────── - describe('Permission check', () => { - it('should change plans when billing manage permission is granted without manager role', async () => { - setupConsoleState({ - isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.manage'], - }) + // ─── 5. Payment capability ────────────────────────────────────────────── + describe('Payment capability', () => { + it('should change plans when payment is allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional, canPay: true }) const button = getPlanButton('billing.plansCommon.startBuilding') await user.click(button) @@ -293,13 +284,9 @@ describe('Cloud Plan Payment Flow', () => { }) }) - it('should show error toast when billing manage permission is missing for plan changes', async () => { - setupConsoleState({ - isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.view', 'billing.subscription.manage'], - }) + it('should block plan changes when payment is not allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional, canPay: false }) const button = getPlanButton('billing.plansCommon.startBuilding') await user.click(button) @@ -310,13 +297,13 @@ describe('Cloud Plan Payment Flow', () => { expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() }) - it('should open billing portal when subscription management permission is granted without manager role', async () => { - setupConsoleState({ - isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.subscription.manage'], - }) + it('should open billing portal when payment is allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional }) + renderCloudPlanItem({ + currentPlan: Plan.professional, + plan: Plan.professional, + canPay: true, + }) const button = getPlanButton('billing.plansCommon.currentPlan') await user.click(button) @@ -327,13 +314,13 @@ describe('Cloud Plan Payment Flow', () => { expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() }) - it('should show error toast when subscription management permission is missing for current paid plan', async () => { - setupConsoleState({ - isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.view', 'billing.manage'], - }) + it('should block billing portal access when payment is not allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional }) + renderCloudPlanItem({ + currentPlan: Plan.professional, + plan: Plan.professional, + canPay: false, + }) const button = getPlanButton('billing.plansCommon.currentPlan') await user.click(button) diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index 79a346a1fc6..1203cc106ae 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -46,10 +46,6 @@ vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) }) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/version-state', async () => { const { createVersionStateModuleMock } = await import('@/test/console/state-fixture') return createVersionStateModuleMock(() => mockConsoleState) @@ -157,7 +153,6 @@ const setupContexts = ( } mockConsoleState = { isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.view', 'billing.manage', 'billing.subscription.manage'], userProfile: { email: 'student@university.edu' }, langGeniusVersionInfo: { current_version: '1.0.0' }, ...appOverrides, @@ -224,9 +219,13 @@ describe('Education Verification Flow', () => { // ─── 2. Successful Verification Flow ──────────────────────────────────── describe('Successful verification flow', () => { - it('should navigate to education-apply with token on successful verification', async () => { + it('should let non-manager members start education verification', async () => { mockMutateAsync.mockResolvedValue({ token: 'edu-token-123' }) - setupContexts({}, { enableEducationPlan: true, isEducationAccount: false }) + setupContexts( + {}, + { enableEducationPlan: true, isEducationAccount: false }, + { isCurrentWorkspaceManager: false }, + ) const user = userEvent.setup() render() diff --git a/web/__tests__/billing/pricing-modal-flow.test.tsx b/web/__tests__/billing/pricing-modal-flow.test.tsx index 595eac8ef53..df6e102df9a 100644 --- a/web/__tests__/billing/pricing-modal-flow.test.tsx +++ b/web/__tests__/billing/pricing-modal-flow.test.tsx @@ -8,7 +8,7 @@ * Validates cross-component state propagation when the user switches between * cloud / self-hosted categories and monthly / yearly plan ranges. */ -import { cleanup, screen } from '@testing-library/react' +import { cleanup, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { ALL_PLANS } from '@/app/components/billing/config' @@ -19,6 +19,7 @@ import { render } from '@/test/console/render' // ─── Mock state ────────────────────────────────────────────────────────────── let mockProviderCtx: Record = {} let mockConsoleState: Record = {} +const mockFetchSubscriptionUrls = vi.hoisted(() => vi.fn()) // ─── Context mocks ─────────────────────────────────────────────────────────── vi.mock('@/context/provider-context', () => ({ @@ -33,10 +34,6 @@ vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => mockConsoleState) }) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => mockConsoleState) -}) vi.mock('@/context/version-state', async () => { const { createVersionStateModuleMock } = await import('@/test/console/state-fixture') return createVersionStateModuleMock(() => mockConsoleState) @@ -49,7 +46,7 @@ vi.mock('@/context/i18n', () => ({ // ─── Service mocks ─────────────────────────────────────────────────────────── vi.mock('@/service/billing', () => ({ - fetchSubscriptionUrls: vi.fn().mockResolvedValue({ url: 'https://pay.example.com' }), + fetchSubscriptionUrls: (...args: unknown[]) => mockFetchSubscriptionUrls(...args), })) vi.mock('@/service/client', () => ({ @@ -131,7 +128,6 @@ const setupContexts = ( } mockConsoleState = { isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.view', 'billing.manage', 'billing.subscription.manage'], userProfile: { email: 'test@example.com' }, langGeniusVersionInfo: { current_version: '1.0.0' }, ...appOverrides, @@ -145,6 +141,7 @@ describe('Pricing Modal Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() + mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://pay.example.com' }) setupContexts() }) @@ -266,6 +263,54 @@ describe('Pricing Modal Flow', () => { // ─── 4. Cloud Plan Button States ───────────────────────────────────────── describe('Cloud plan button states', () => { + it('should allow managers without billing permission keys to change plans', async () => { + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) + + await waitFor(() => { + expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'month') + }) + }) + + it('should default education account managers to yearly checkout', async () => { + setupContexts() + mockProviderCtx = { + ...mockProviderCtx, + enableEducationPlan: true, + isEducationAccount: true, + } + const user = userEvent.setup() + render() + + expect(screen.getByRole('switch')).toBeChecked() + + await user.click(screen.getByRole('button', { name: 'education.useEducationDiscount' })) + + await waitFor(() => { + expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'year') + }) + }) + + it('should block non-manager members even when billing permission keys are present', async () => { + setupContexts( + {}, + { + isCurrentWorkspaceManager: false, + workspacePermissionKeys: ['billing.manage', 'billing.subscription.manage'], + }, + ) + const user = userEvent.setup() + render() + + await user.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) + + await waitFor(() => { + expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + }) + }) + it('should show "Current Plan" for the current plan (sandbox)', () => { setupContexts({ type: Plan.sandbox }) render() diff --git a/web/__tests__/billing/self-hosted-plan-flow.test.tsx b/web/__tests__/billing/self-hosted-plan-flow.test.tsx index 3be64c06f6b..eab27209e1e 100644 --- a/web/__tests__/billing/self-hosted-plan-flow.test.tsx +++ b/web/__tests__/billing/self-hosted-plan-flow.test.tsx @@ -1,14 +1,12 @@ -import { toast, ToastHost } from '@langgenius/dify-ui/toast' /** * Integration test: Self-Hosted Plan Flow * * Tests the self-hosted plan items: - * SelfHostedPlanItem → Button click → permission check → redirect to external URL + * SelfHostedPlanItem → Button click → redirect to external URL * - * Covers community/premium/enterprise plan rendering, external URL navigation, - * and workspace manager permission enforcement. + * Covers community/premium/enterprise plan rendering and external URL navigation. */ -import { cleanup, screen, waitFor } from '@testing-library/react' +import { cleanup, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { @@ -20,20 +18,9 @@ import SelfHostedPlanItem from '@/app/components/billing/pricing/plans/self-host import { SelfHostedPlan } from '@/app/components/billing/type' import { render } from '@/test/console/render' -let mockConsoleState: Record = {} - const originalLocation = window.location let assignedHref = '' -vi.mock('@/context/workspace-state', async () => { - const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') - return createWorkspaceStateModuleMock(() => mockConsoleState) -}) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => mockConsoleState) -}) - vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), useTheme: () => ({ theme: 'light' }), @@ -52,29 +39,14 @@ vi.mock('@/app/components/billing/pricing/plans/self-hosted-plan-item/list', () ), })) -const setupConsoleState = (overrides: Record = {}) => { - mockConsoleState = { - isCurrentWorkspaceManager: true, - workspacePermissionKeys: ['billing.manage'], - ...overrides, - } -} - const renderSelfHostedPlanItem = (plan: SelfHostedPlan) => { - return render( - <> - - - , - ) + return render() } describe('Self-Hosted Plan Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() - toast.dismiss() - setupConsoleState() // Mock window.location with minimal getter/setter (Location props are non-enumerable) assignedHref = '' @@ -187,64 +159,4 @@ describe('Self-Hosted Plan Flow', () => { expect(assignedHref).toBe(contactSalesUrl) }) }) - - // ─── 3. Permission Check ──────────────────────────────────────────────── - describe('Permission check', () => { - it('should redirect when billing manage permission is granted without manager role', async () => { - setupConsoleState({ - isCurrentWorkspaceManager: false, - workspacePermissionKeys: ['billing.manage'], - }) - const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.community) - - const button = screen.getByRole('button') - await user.click(button) - - expect(assignedHref).toBe(getStartedWithCommunityUrl) - }) - - it('should show error toast when billing manage permission is missing for community button', async () => { - setupConsoleState({ workspacePermissionKeys: [] }) - const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.community) - - const button = screen.getByRole('button') - await user.click(button) - - await waitFor(() => { - expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() - }) - // Should NOT redirect - expect(assignedHref).toBe('') - }) - - it('should show error toast when billing manage permission is missing for premium button', async () => { - setupConsoleState({ workspacePermissionKeys: [] }) - const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.premium) - - const button = screen.getByRole('button') - await user.click(button) - - await waitFor(() => { - expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() - }) - expect(assignedHref).toBe('') - }) - - it('should show error toast when billing manage permission is missing for enterprise button', async () => { - setupConsoleState({ workspacePermissionKeys: [] }) - const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.enterprise) - - const button = screen.getByRole('button') - await user.click(button) - - await waitFor(() => { - expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() - }) - expect(assignedHref).toBe('') - }) - }) }) diff --git a/web/app/components/billing/billing-page/__tests__/index.spec.tsx b/web/app/components/billing/billing-page/__tests__/index.spec.tsx index e9f30459d73..bb30271955e 100644 --- a/web/app/components/billing/billing-page/__tests__/index.spec.tsx +++ b/web/app/components/billing/billing-page/__tests__/index.spec.tsx @@ -6,7 +6,6 @@ let currentBillingUrl: string | null = 'https://billing' let fetching = false let isManager = true let enableBilling = true -let workspacePermissionKeys: string[] = ['billing.subscription.manage'] let billingUrlEnabled = false const refetchMock = vi.fn() @@ -39,14 +38,6 @@ vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') return createWorkspaceStateModuleMock(() => ({ isCurrentWorkspaceManager: isManager, - workspacePermissionKeys, - })) -}) -vi.mock('@/context/permission-state', async () => { - const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') - return createPermissionStateModuleMock(() => ({ - isCurrentWorkspaceManager: isManager, - workspacePermissionKeys, })) }) @@ -68,11 +59,10 @@ describe('Billing', () => { isManager = true enableBilling = true billingUrlEnabled = false - workspacePermissionKeys = ['billing.subscription.manage'] refetchMock.mockResolvedValue({ data: 'https://billing' }) }) - it('hides the billing action when subscription management permission is granted without manager role', () => { + it('hides the billing action from non-manager members', () => { isManager = false render() @@ -83,16 +73,14 @@ describe('Billing', () => { expect(billingUrlEnabled).toBe(false) }) - it('hides the billing action when subscription management permission is missing or billing is disabled', () => { - workspacePermissionKeys = [] + it('shows the billing action to managers without billing permission keys', () => { render() - expect( - screen.queryByRole('button', { name: /billing\.viewBillingTitle/ }), - ).not.toBeInTheDocument() - expect(billingUrlEnabled).toBe(false) - vi.clearAllMocks() - workspacePermissionKeys = ['billing.subscription.manage'] + expect(screen.getByRole('button', { name: /billing\.viewBillingTitle/ })).toBeInTheDocument() + expect(billingUrlEnabled).toBe(true) + }) + + it('hides the billing action when billing is disabled', () => { enableBilling = false render() expect( diff --git a/web/app/components/billing/billing-page/index.tsx b/web/app/components/billing/billing-page/index.tsx index dec66dd2ead..e9877b21404 100644 --- a/web/app/components/billing/billing-page/index.tsx +++ b/web/app/components/billing/billing-page/index.tsx @@ -3,27 +3,21 @@ import type { FC } from 'react' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext } from '@/context/provider-context' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { useBillingUrl } from '@/service/use-billing' -import { BillingPermission, hasPermission } from '@/utils/permission' import PlanComp from '../plan' const Billing: FC = () => { const { t } = useTranslation() const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) - const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const { enableBilling } = useProviderContext() - const canManageBillingSubscription = - isCurrentWorkspaceManager && - hasPermission(workspacePermissionKeys, BillingPermission.SubscriptionManage) const { data: billingUrl, isFetching, refetch, - } = useBillingUrl(enableBilling && canManageBillingSubscription) + } = useBillingUrl(enableBilling && isCurrentWorkspaceManager) const openAsyncWindow = useAsyncWindowOpen() const handleOpenBilling = async () => { @@ -46,7 +40,7 @@ const Billing: FC = () => { return (
- {enableBilling && canManageBillingSubscription && ( + {enableBilling && isCurrentWorkspaceManager && (