diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py index bee3cdddbef..da162ac49a8 100644 --- a/api/controllers/console/billing/billing.py +++ b/api/controllers/console/billing/billing.py @@ -40,14 +40,18 @@ class BillingInvoiceResponse(ResponseModel): url: str +class BillingSubscriptionResponse(ResponseModel): + url: str + + register_schema_models(console_ns, SubscriptionQuery, PartnerTenantsPayload) -register_response_schema_models(console_ns, BillingResponse, BillingInvoiceResponse) +register_response_schema_models(console_ns, BillingResponse, BillingInvoiceResponse, BillingSubscriptionResponse) @console_ns.route("/billing/subscription") class Subscription(Resource): @console_ns.doc(params=query_params_from_model(SubscriptionQuery)) - @console_ns.response(200, "Success", console_ns.models[BillingResponse.__name__]) + @console_ns.response(200, "Success", console_ns.models[BillingSubscriptionResponse.__name__]) @setup_required @login_required @account_initialization_required diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 31781616af3..4ded2d8fd85 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -38,6 +38,7 @@ from controllers.console.wraps import ( with_current_tenant_id, with_current_user, ) +from enums import CloudPlan from extensions.ext_application_services import application_services from extensions.ext_database import db from fields.base import ResponseModel @@ -80,7 +81,7 @@ class WorkspaceInfoPayload(BaseModel): class TenantInfoResponse(ResponseModel): id: str name: str | None = None - plan: str | None = None + plan: CloudPlan | None = None status: str | None = None created_at: int | None = None role: str | None = None @@ -92,7 +93,7 @@ class TenantInfoResponse(ResponseModel): trial_credits_exhausted_at: int | None = None next_credit_reset_date: int | None = None - @field_validator("plan", "status", "trial_end_reason", mode="before") + @field_validator("status", "trial_end_reason", mode="before") @classmethod def _normalize_enum_like(cls, value): if value is None: @@ -111,20 +112,20 @@ class CurrentWorkspaceSummaryResponse(ResponseModel): id: str name: str role: TenantAccountRole - plan: str | None + plan: CloudPlan | None credits: int | None = Field(description="Remaining credits in the effective pool; -1 means unlimited.") class TenantListItemResponse(ResponseModel): id: str name: str | None = None - plan: str | None = None + plan: CloudPlan | None = None status: str | None = None created_at: int | None = None last_opened_at: int | None = None current: bool - @field_validator("plan", "status", mode="before") + @field_validator("status", mode="before") @classmethod def _normalize_enum_like(cls, value): if value is None: diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 496a3612add..c2d159ec4c5 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -5352,7 +5352,7 @@ Sync partner tenants bindings | Code | Description | Schema | | ---- | ----------- | ------ | -| 200 | Success | **application/json**: [BillingResponse](#billingresponse)
| +| 200 | Success | **application/json**: [BillingSubscriptionResponse](#billingsubscriptionresponse)
| ### [GET] /code-based-extension Get code-based extension data by module name @@ -15956,6 +15956,12 @@ Retrieval settings for Amazon Bedrock knowledge base queries. | ---- | ---- | ----------- | -------- | | BillingResponse | object | | | +#### BillingSubscriptionResponse + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| url | string | | Yes | + #### BinaryFileResponse | Name | Type | Description | Required | @@ -16194,6 +16200,18 @@ Button styles for user actions. | install_commands | [ string ] | | No | | name | string | | Yes | +#### CloudPlan + +Enum representing user plan types in the cloud platform. + +SANDBOX: Free/default plan with limited features +PROFESSIONAL: Professional paid plan +TEAM: Team collaboration paid plan + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| CloudPlan | string | Enum representing user plan types in the cloud platform. SANDBOX: Free/default plan with limited features PROFESSIONAL: Professional paid plan TEAM: Team collaboration paid plan | | + #### CodeBasedExtensionQuery | Name | Type | Description | Required | @@ -16649,7 +16667,7 @@ Model class for credential form schema. | credits | integer | Remaining credits in the effective pool; -1 means unlimited. | Yes | | id | string | | Yes | | name | string | | Yes | -| plan | string | | Yes | +| plan | [CloudPlan](#cloudplan) | | Yes | | role | [TenantAccountRole](#tenantaccountrole) | | Yes | #### CustomConfigurationResponse @@ -22211,7 +22229,7 @@ The subscription constructor of the trigger provider | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | interval | string | | Yes | -| plan | string,
**Default:** sandbox | | Yes | +| plan | [CloudPlan](#cloudplan) | | Yes | #### SubscriptionQuery @@ -22425,7 +22443,7 @@ Tag type | in_trial | boolean | | No | | name | string | | No | | next_credit_reset_date | integer | | No | -| plan | string | | No | +| plan | [CloudPlan](#cloudplan) | | No | | role | string | | No | | status | string | | No | | trial_credits | integer | | No | @@ -22442,7 +22460,7 @@ Tag type | id | string | | Yes | | last_opened_at | integer | | No | | name | string | | No | -| plan | string | | No | +| plan | [CloudPlan](#cloudplan) | | No | | status | string | | No | #### TenantListResponse diff --git a/api/services/entities/feature_entities.py b/api/services/entities/feature_entities.py index a4ff3c57eba..666943a5276 100644 --- a/api/services/entities/feature_entities.py +++ b/api/services/entities/feature_entities.py @@ -12,7 +12,7 @@ class FeatureResponseModel(BaseModel): class SubscriptionModel(FeatureResponseModel): - plan: str = CloudPlan.SANDBOX + plan: CloudPlan = CloudPlan.SANDBOX interval: str = "" diff --git a/api/services/feature_service.py b/api/services/feature_service.py index ec94f28630d..b32387ffea0 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -220,7 +220,7 @@ class FeatureService: features_usage_info = BillingService.get_quota_info(tenant_id) features.billing.enabled = billing_info["enabled"] - features.billing.subscription.plan = billing_info["subscription"]["plan"] + features.billing.subscription.plan = CloudPlan(billing_info["subscription"]["plan"]) features.billing.subscription.interval = billing_info["subscription"]["interval"] features.education.activated = billing_info["subscription"].get("education", False) diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 15c65456ffd..855dcdd1131 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -15,7 +15,7 @@ from services.feature_service import FeatureService @dataclass(frozen=True) class EffectiveCreditPool: - plan: str | None = None + plan: CloudPlan | None = None pool_type: Literal["paid", "trial"] | None = None quota_limit: int | None = None quota_used: int | None = None @@ -56,7 +56,7 @@ class WorkspaceService: return EffectiveCreditPool() billing_info = BillingService.get_info(tenant_id, exclude_vector_space=True) - subscription_plan: str = billing_info["subscription"]["plan"] + subscription_plan = CloudPlan(billing_info["subscription"]["plan"]) from services.credit_pool_service import CreditPoolBalance, CreditPoolService diff --git a/api/tests/test_containers_integration_tests/services/test_feature_service.py b/api/tests/test_containers_integration_tests/services/test_feature_service.py index a314be8d8c5..ca933a08462 100644 --- a/api/tests/test_containers_integration_tests/services/test_feature_service.py +++ b/api/tests/test_containers_integration_tests/services/test_feature_service.py @@ -29,7 +29,7 @@ class TestFeatureService: # Setup default mock returns for BillingService mock_billing_service.get_info.return_value = { "enabled": True, - "subscription": {"plan": "pro", "interval": "monthly", "education": True}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": True}, "members": {"size": 5, "limit": 10}, "apps": {"size": 3, "limit": 20}, "vector_space": {"size": 2, "limit": 10}, @@ -41,7 +41,10 @@ class TestFeatureService: "knowledge_rate_limit": {"limit": 100}, } - mock_billing_service.get_knowledge_rate_limit.return_value = {"limit": 100, "subscription_plan": "pro"} + mock_billing_service.get_knowledge_rate_limit.return_value = { + "limit": 100, + "subscription_plan": CloudPlan.PROFESSIONAL, + } # Setup default mock returns for EnterpriseService mock_enterprise_service.get_workspace_info.return_value = { @@ -114,7 +117,7 @@ class TestFeatureService: # Verify billing features assert result.billing.enabled is True - assert result.billing.subscription.plan == "pro" + assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL assert result.billing.subscription.interval == "monthly" assert result.education.activated is True @@ -250,7 +253,7 @@ class TestFeatureService: # Verify rate limit configuration assert result.enabled is True assert result.limit == 100 - assert result.subscription_plan == "pro" + assert result.subscription_plan == CloudPlan.PROFESSIONAL # Verify mock interactions mock_external_service_dependencies["billing_service"].get_knowledge_rate_limit.assert_called_once_with( @@ -749,7 +752,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "basic", "interval": "yearly"}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"}, # Missing members, apps, vector_space, etc. } @@ -762,7 +765,7 @@ class TestFeatureService: # Verify billing features assert result.billing.enabled is True - assert result.billing.subscription.plan == "basic" + assert result.billing.subscription.plan == CloudPlan.PROFESSIONAL assert result.billing.subscription.interval == "yearly" # Verify default values for missing billing info @@ -779,7 +782,7 @@ class TestFeatureService: assert result.knowledge_rate_limit == 10 assert result.docs_processing == "standard" - # Verify basic plan restrictions (non-sandbox plans have webapp copyright enabled) + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -810,7 +813,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "pro", "interval": "monthly"}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"}, "vector_space": {"size": 0, "limit": 0}, "apps": {"size": 5, "limit": 10}, } @@ -830,7 +833,7 @@ class TestFeatureService: assert result.apps.size == 5 assert result.apps.limit == 10 - # Verify pro plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -927,7 +930,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "basic", "interval": "yearly"}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "yearly"}, "members": {"size": 10, "limit": 10}, "vector_space": {"size": 3, "limit": 5}, } @@ -947,7 +950,7 @@ class TestFeatureService: assert result.vector_space.size == 3 assert result.vector_space.limit == 5 - # Verify basic plan features (non-sandbox plans have webapp copyright enabled) + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1247,7 +1250,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "enterprise", "interval": "yearly"}, + "subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"}, "members": {"size": 0, "limit": 0}, "apps": {"size": 0, "limit": -1}, "vector_space": {"size": 0, "limit": 999999}, @@ -1274,7 +1277,7 @@ class TestFeatureService: assert result.annotation_quota_limit.size == 0 assert result.annotation_quota_limit.limit == 1 - # Verify enterprise plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1351,7 +1354,7 @@ class TestFeatureService: tenant_id = self._create_test_tenant_id() mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "education", "interval": "semester", "education": True}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "semester", "education": True}, "members": {"size": 100, "limit": 200}, "apps": {"size": 50, "limit": 100}, "vector_space": {"size": 20, "limit": 50}, @@ -1374,7 +1377,7 @@ class TestFeatureService: assert result.education.enabled is True assert result.education.activated is True - # Verify education plan limits + # Verify education subscription limits. assert result.members.size == 100 assert result.members.limit == 200 assert result.apps.size == 50 @@ -1386,7 +1389,7 @@ class TestFeatureService: assert result.annotation_quota_limit.size == 200 assert result.annotation_quota_limit.limit == 500 - # Verify education plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1505,7 +1508,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "premium", "interval": "monthly"}, + "subscription": {"plan": CloudPlan.TEAM, "interval": "monthly"}, "docs_processing": "advanced", "can_replace_logo": True, "model_load_balancing_enabled": True, @@ -1523,7 +1526,7 @@ class TestFeatureService: assert result.can_replace_logo is True assert result.model_load_balancing_enabled is True - # Verify premium plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1623,7 +1626,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "enterprise", "interval": "yearly"}, + "subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"}, "annotation_quota_limit": {"size": 999, "limit": 1000}, "knowledge_rate_limit": {"limit": 500}, } @@ -1642,7 +1645,7 @@ class TestFeatureService: # Verify knowledge rate limit assert result.knowledge_rate_limit == 500 - # Verify enterprise plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1684,7 +1687,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, - "subscription": {"plan": "pro", "interval": "monthly"}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly"}, "documents_upload_quota": { "size": 0, # Edge case: zero current size "limit": 0, # Edge case: zero limit @@ -1706,7 +1709,7 @@ class TestFeatureService: # Verify knowledge rate limit assert result.knowledge_rate_limit == 100 - # Verify pro plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True @@ -1800,7 +1803,7 @@ class TestFeatureService: mock_external_service_dependencies["billing_service"].get_info.return_value = { "enabled": True, "subscription": { - "plan": "pro", + "plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": False, # Education explicitly disabled }, @@ -1820,7 +1823,7 @@ class TestFeatureService: # Verify knowledge rate limit assert result.knowledge_rate_limit == 100 - # Verify pro plan features + # Verify paid plan behavior. assert result.webapp_copyright_enabled is True assert result.is_allow_transfer_workspace is True diff --git a/api/tests/unit_tests/services/test_feature_entities.py b/api/tests/unit_tests/services/test_feature_entities.py index 393e427d751..4b04d837036 100644 --- a/api/tests/unit_tests/services/test_feature_entities.py +++ b/api/tests/unit_tests/services/test_feature_entities.py @@ -1,6 +1,17 @@ import pytest +from pydantic import ValidationError -from services.entities.feature_entities import LicenseLimitationModel +from enums import CloudPlan +from services.entities.feature_entities import LicenseLimitationModel, SubscriptionModel + + +def test_subscription_model_uses_the_cloud_plan_value_set() -> None: + subscription = SubscriptionModel(plan="team") + + assert subscription.plan is CloudPlan.TEAM + + with pytest.raises(ValidationError): + SubscriptionModel(plan="unknown") @pytest.mark.parametrize( diff --git a/api/tests/unit_tests/services/test_feature_service_vector_space.py b/api/tests/unit_tests/services/test_feature_service_vector_space.py index 9cba1175f87..54f98d4b6db 100644 --- a/api/tests/unit_tests/services/test_feature_service_vector_space.py +++ b/api/tests/unit_tests/services/test_feature_service_vector_space.py @@ -1,7 +1,7 @@ from typing import cast from unittest.mock import patch -from enums import DeploymentEdition +from enums import CloudPlan, DeploymentEdition from services.billing_service import BillingInfo from services.entities.feature_entities import LimitationModel from services.feature_service import FeatureService @@ -11,7 +11,7 @@ def test_get_features_exclude_vector_space_sets_vector_space_to_none(): tenant_id = "tenant-id" billing_info = { "enabled": True, - "subscription": {"plan": "pro", "interval": "monthly", "education": False}, + "subscription": {"plan": CloudPlan.PROFESSIONAL, "interval": "monthly", "education": False}, "members": {"size": 1, "limit": 10}, "apps": {"size": 2, "limit": 20}, "documents_upload_quota": {"size": 3, "limit": 100}, diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index c83bf1990d5..2bd94977d9e 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1838,7 +1838,7 @@ }, "web/app/components/billing/plan/assets/index.tsx": { "no-barrel-files/no-barrel-files": { - "count": 4 + "count": 3 } }, "web/app/components/billing/pricing/assets/index.tsx": { @@ -1864,11 +1864,6 @@ "count": 1 } }, - "web/app/components/billing/type.ts": { - "erasable-syntax-only/enums": { - "count": 4 - } - }, "web/app/components/datasets/chunk.tsx": { "jsx_a11y/label-has-associated-control": { "count": 2 @@ -5580,11 +5575,6 @@ "count": 3 } }, - "web/service/billing.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/service/common.ts": { "no-restricted-imports": { "count": 1 diff --git a/packages/contracts/generated/api/console/billing/types.gen.ts b/packages/contracts/generated/api/console/billing/types.gen.ts index dc7afb41d0f..7410402d897 100644 --- a/packages/contracts/generated/api/console/billing/types.gen.ts +++ b/packages/contracts/generated/api/console/billing/types.gen.ts @@ -16,6 +16,10 @@ export type BillingResponse = { [key: string]: unknown } +export type BillingSubscriptionResponse = { + url: string +} + export type GetBillingInvoicesData = { body?: never path?: never @@ -61,7 +65,7 @@ export type GetBillingSubscriptionData = { } export type GetBillingSubscriptionResponses = { - 200: BillingResponse + 200: BillingSubscriptionResponse } export type GetBillingSubscriptionResponse = diff --git a/packages/contracts/generated/api/console/billing/zod.gen.ts b/packages/contracts/generated/api/console/billing/zod.gen.ts index a25890905c0..87d75a92621 100644 --- a/packages/contracts/generated/api/console/billing/zod.gen.ts +++ b/packages/contracts/generated/api/console/billing/zod.gen.ts @@ -21,6 +21,13 @@ export const zPartnerTenantsPayload = z.object({ */ export const zBillingResponse = z.record(z.string(), z.unknown()) +/** + * BillingSubscriptionResponse + */ +export const zBillingSubscriptionResponse = z.object({ + url: z.string(), +}) + /** * Success */ @@ -45,4 +52,4 @@ export const zGetBillingSubscriptionQuery = z.object({ /** * Success */ -export const zGetBillingSubscriptionResponse = zBillingResponse +export const zGetBillingSubscriptionResponse = zBillingSubscriptionResponse diff --git a/packages/contracts/generated/api/console/features/types.gen.ts b/packages/contracts/generated/api/console/features/types.gen.ts index c78ebbca40c..bcc666d3f40 100644 --- a/packages/contracts/generated/api/console/features/types.gen.ts +++ b/packages/contracts/generated/api/console/features/types.gen.ts @@ -66,9 +66,11 @@ export type LicenseLimitationModel = { export type SubscriptionModel = { interval: string - plan: string + plan: CloudPlan } +export type CloudPlan = 'professional' | 'sandbox' | 'team' + export type GetFeaturesData = { body?: never path?: never diff --git a/packages/contracts/generated/api/console/features/zod.gen.ts b/packages/contracts/generated/api/console/features/zod.gen.ts index 6e4e713b049..0e248a8dee3 100644 --- a/packages/contracts/generated/api/console/features/zod.gen.ts +++ b/packages/contracts/generated/api/console/features/zod.gen.ts @@ -56,12 +56,23 @@ export const zLicenseLimitationModel = z.object({ size: z.int().default(0), }) +/** + * CloudPlan + * + * Enum representing user plan types in the cloud platform. + * + * SANDBOX: Free/default plan with limited features + * PROFESSIONAL: Professional paid plan + * TEAM: Team collaboration paid plan + */ +export const zCloudPlan = z.enum(['professional', 'sandbox', 'team']) + /** * SubscriptionModel */ export const zSubscriptionModel = z.object({ interval: z.string().default(''), - plan: z.string().default('sandbox'), + plan: zCloudPlan.default('sandbox'), }) /** diff --git a/packages/contracts/generated/api/console/workspaces/types.gen.ts b/packages/contracts/generated/api/console/workspaces/types.gen.ts index 7565298870a..a7c53ab7a4b 100644 --- a/packages/contracts/generated/api/console/workspaces/types.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/types.gen.ts @@ -646,7 +646,7 @@ export type CurrentWorkspaceSummaryResponse = { credits: number | null id: string name: string - plan: string | null + plan: CloudPlan | null role: TenantAccountRole } @@ -1053,7 +1053,7 @@ export type TenantListItemResponse = { id: string last_opened_at?: number | null name?: string | null - plan?: string | null + plan?: CloudPlan | null status?: string | null } @@ -1541,6 +1541,8 @@ export type AccessPolicyRole = { role_tag?: string } +export type CloudPlan = 'professional' | 'sandbox' | 'team' + export type TenantAccountRole = 'admin' | 'dataset_operator' | 'editor' | 'normal' | 'owner' export type ToolLabel = { @@ -1709,7 +1711,7 @@ export type TenantInfoResponse = { in_trial?: boolean | null name?: string | null next_credit_reset_date?: number | null - plan?: string | null + plan?: CloudPlan | null role?: string | null status?: string | null trial_credits?: number | null diff --git a/packages/contracts/generated/api/console/workspaces/zod.gen.ts b/packages/contracts/generated/api/console/workspaces/zod.gen.ts index 5abe9e31082..8bcd255b3c9 100644 --- a/packages/contracts/generated/api/console/workspaces/zod.gen.ts +++ b/packages/contracts/generated/api/console/workspaces/zod.gen.ts @@ -623,26 +623,6 @@ export const zSwitchWorkspacePayload = z.object({ tenant_id: z.string(), }) -/** - * TenantListItemResponse - */ -export const zTenantListItemResponse = z.object({ - created_at: z.int().nullish(), - current: z.boolean(), - id: z.string(), - last_opened_at: z.int().nullish(), - name: z.string().nullish(), - plan: z.string().nullish(), - status: z.string().nullish(), -}) - -/** - * TenantListResponse - */ -export const zTenantListResponse = z.object({ - workspaces: z.array(zTenantListItemResponse), -}) - /** * IconInfo * @@ -1231,6 +1211,37 @@ export const zWorkspaceAccessMatrix = z.object({ pagination: zPagination.nullish(), }) +/** + * CloudPlan + * + * Enum representing user plan types in the cloud platform. + * + * SANDBOX: Free/default plan with limited features + * PROFESSIONAL: Professional paid plan + * TEAM: Team collaboration paid plan + */ +export const zCloudPlan = z.enum(['professional', 'sandbox', 'team']) + +/** + * TenantListItemResponse + */ +export const zTenantListItemResponse = z.object({ + created_at: z.int().nullish(), + current: z.boolean(), + id: z.string(), + last_opened_at: z.int().nullish(), + name: z.string().nullish(), + plan: zCloudPlan.nullish(), + status: z.string().nullish(), +}) + +/** + * TenantListResponse + */ +export const zTenantListResponse = z.object({ + workspaces: z.array(zTenantListItemResponse), +}) + /** * TenantAccountRole */ @@ -1243,7 +1254,7 @@ export const zCurrentWorkspaceSummaryResponse = z.object({ credits: z.int().nullable(), id: z.string(), name: z.string(), - plan: z.string().nullable(), + plan: zCloudPlan.nullable(), role: zTenantAccountRole, }) @@ -1566,7 +1577,7 @@ export const zTenantInfoResponse = z.object({ in_trial: z.boolean().nullish(), name: z.string().nullish(), next_credit_reset_date: z.int().nullish(), - plan: z.string().nullish(), + plan: zCloudPlan.nullish(), role: z.string().nullish(), status: z.string().nullish(), trial_credits: z.int().nullish(), diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index 58bf3d2fa4c..248d18ddaf3 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -1,4 +1,5 @@ -import type { Plan, UsagePlanInfo } from '@/app/components/billing/type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { UsagePlanInfo } from '@/app/components/billing/type' import type { ProviderContextState } from '@/context/provider-context' import { merge } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' @@ -21,17 +22,8 @@ export const baseProviderContextValue: ProviderContextState = { onPlanInfoChanged: noop, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, - datasetOperatorEnabled: false, enableEducationPlan: false, - isEducationWorkspace: false, webappCopyrightEnabled: false, - licenseLimit: { - workspace_members: { - size: 0, - limit: 0, - }, - }, - refreshLicenseLimit: noop, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, @@ -46,11 +38,10 @@ export const createMockProviderContextValue = ( ...merged, refreshModelProviders: merged.refreshModelProviders ?? noop, onPlanInfoChanged: merged.onPlanInfoChanged ?? noop, - refreshLicenseLimit: merged.refreshLicenseLimit ?? noop, } } -export const createMockPlan = (plan: Plan): ProviderContextState => +export const createMockPlan = (plan: CloudPlan): ProviderContextState => createMockProviderContextValue({ plan: merge({}, defaultPlan, { type: plan, diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index b15f485f40c..30531a83d98 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -13,7 +13,6 @@ import PlanComp from '@/app/components/billing/plan' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' import PriorityLabel from '@/app/components/billing/priority-label' import TriggerEventsLimitModal from '@/app/components/billing/trigger-events-limit-modal' -import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import VectorSpaceFull from '@/app/components/billing/vector-space-full' import { consoleQuery } from '@/service/client' @@ -148,7 +147,7 @@ describe('Billing Page + Plan Integration', () => { describe('Rendering complete plan information', () => { it('should display all 7 usage metrics for sandbox plan', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 3, teamMembers: 1, @@ -186,7 +185,7 @@ describe('Billing Page + Plan Integration', () => { it('should expose each quota card and its value through stable semantics', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { teamMembers: 3 }, total: { teamMembers: 5 }, }) @@ -204,7 +203,7 @@ describe('Billing Page + Plan Integration', () => { it('should display unknown vector space usage as a placeholder', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { vectorSpace: 0 }, total: { vectorSpace: 50 }, }) @@ -219,7 +218,7 @@ describe('Billing Page + Plan Integration', () => { it('should show "unlimited" for infinite quotas (professional API rate limit)', () => { setupProviderContext({ - type: Plan.professional, + type: 'professional', total: { apiRateLimit: NUM_INFINITE }, }) @@ -230,7 +229,7 @@ describe('Billing Page + Plan Integration', () => { it('should display reset days for trigger events when applicable', () => { setupProviderContext({ - type: Plan.professional, + type: 'professional', total: { triggerEvents: 20000 }, reset: { triggerEvents: 7 }, }) @@ -245,7 +244,7 @@ describe('Billing Page + Plan Integration', () => { // Verify billing URL button visibility and behavior describe('Billing URL button', () => { it('should show billing button to managers without billing permission keys', () => { - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [], @@ -258,7 +257,7 @@ describe('Billing Page + Plan Integration', () => { }) it('should hide billing button from non-manager members', () => { - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) setupConsoleState({ isCurrentWorkspaceManager: false, }) @@ -269,7 +268,7 @@ describe('Billing Page + Plan Integration', () => { }) it('should show billing button when a manager has no billing permission keys', () => { - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [], @@ -281,7 +280,7 @@ describe('Billing Page + Plan Integration', () => { }) it('should hide billing button when billing is disabled', () => { - setupProviderContext({ type: Plan.sandbox }, { enableBilling: false }) + setupProviderContext({ type: 'sandbox' }, { enableBilling: false }) render() @@ -301,7 +300,7 @@ describe('Plan Type Display Integration', () => { }) it('should render sandbox plan with upgrade button (premium badge)', () => { - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) render() @@ -312,7 +311,7 @@ describe('Plan Type Display Integration', () => { }) it('should render professional plan with plain upgrade button', () => { - setupProviderContext({ type: Plan.professional }) + setupProviderContext({ type: 'professional' }) render() @@ -322,7 +321,7 @@ describe('Plan Type Display Integration', () => { }) it('should render team plan with plain-style upgrade button', () => { - setupProviderContext({ type: Plan.team }) + setupProviderContext({ type: 'team' }) render() @@ -331,17 +330,8 @@ describe('Plan Type Display Integration', () => { expect(screen.getByText(/upgradeBtn\.plain/i)).toBeInTheDocument() }) - it('should not render upgrade button for enterprise plan', () => { - setupProviderContext({ type: Plan.enterprise }) - - render() - - expect(screen.queryByText(/upgradeBtn\.encourageShort/i)).not.toBeInTheDocument() - expect(screen.queryByText(/upgradeBtn\.plain/i)).not.toBeInTheDocument() - }) - it('should show education verify button when enableEducationPlan is true and not yet verified', () => { - setupProviderContext({ type: Plan.sandbox }, { enableEducationPlan: true }) + setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }) render() @@ -349,11 +339,7 @@ describe('Plan Type Display Integration', () => { }) it('should show education discount to managers without billing permission keys', () => { - setupProviderContext( - { type: Plan.sandbox }, - { enableEducationPlan: true }, - { is_student: true }, - ) + setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }, { is_student: true }) setupConsoleState({ isCurrentWorkspaceManager: true, workspacePermissionKeys: [] }) render() @@ -362,11 +348,7 @@ describe('Plan Type Display Integration', () => { }) it('should hide education discount from non-manager members', () => { - setupProviderContext( - { type: Plan.sandbox }, - { enableEducationPlan: true }, - { is_student: true }, - ) + setupProviderContext({ type: 'sandbox' }, { enableEducationPlan: true }, { is_student: true }) setupConsoleState({ isCurrentWorkspaceManager: false, workspacePermissionKeys: ['billing.manage'], @@ -387,7 +369,7 @@ describe('Upgrade Flow Integration', () => { beforeEach(() => { vi.clearAllMocks() setupConsoleState() - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) }) // UpgradeBtn triggers pricing modal @@ -513,7 +495,7 @@ describe('Upgrade Flow Integration', () => { describe('PlanComp upgrade button triggers pricing', () => { it('should open pricing modal when clicking upgrade in sandbox plan', async () => { const user = userEvent.setup() - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) render() @@ -540,7 +522,7 @@ describe('Capacity Full Components Integration', () => { describe('AppsFull integration', () => { it('should display upgrade tip and upgrade button for sandbox plan at capacity', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 5 }, total: { buildApps: 5 }, }) @@ -559,7 +541,7 @@ describe('Capacity Full Components Integration', () => { it('should display upgrade tip and upgrade button for professional plan', () => { setupProviderContext({ - type: Plan.professional, + type: 'professional', usage: { buildApps: 48 }, total: { buildApps: 50 }, }) @@ -572,7 +554,7 @@ describe('Capacity Full Components Integration', () => { it('should display contact tip and contact button for team plan', () => { setupProviderContext({ - type: Plan.team, + type: 'team', usage: { buildApps: 200 }, total: { buildApps: 200 }, }) @@ -589,7 +571,7 @@ describe('Capacity Full Components Integration', () => { it('should render progress bar with correct color based on usage percentage', () => { // 100% usage should show error color setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 5 }, total: { buildApps: 5 }, }) @@ -604,7 +586,7 @@ describe('Capacity Full Components Integration', () => { describe('VectorSpaceFull integration', () => { it('should display full tip, upgrade button, and vector space usage info', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { vectorSpace: 50 }, total: { vectorSpace: 50 }, }) @@ -625,7 +607,7 @@ describe('Capacity Full Components Integration', () => { describe('AnnotationFull integration', () => { it('should display annotation full tip, upgrade button, and usage info', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { annotatedResponse: 10 }, total: { annotatedResponse: 10 }, }) @@ -645,7 +627,7 @@ describe('Capacity Full Components Integration', () => { describe('AnnotationFullModal integration', () => { it('should render modal with annotation info and upgrade button when show is true', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { annotatedResponse: 10 }, total: { annotatedResponse: 10 }, }) @@ -659,7 +641,7 @@ describe('Capacity Full Components Integration', () => { it('should not render content when show is false', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { annotatedResponse: 10 }, total: { annotatedResponse: 10 }, }) @@ -673,7 +655,7 @@ describe('Capacity Full Components Integration', () => { // TriggerEventsLimitModal renders PlanUpgradeModal with embedded UsageInfo describe('TriggerEventsLimitModal integration', () => { it('should display trigger limit title, usage info, and upgrade button', () => { - setupProviderContext({ type: Plan.professional }) + setupProviderContext({ type: 'professional' }) render( { const user = userEvent.setup() const onClose = vi.fn() const onUpgrade = vi.fn() - setupProviderContext({ type: Plan.professional }) + setupProviderContext({ type: 'professional' }) render( { }) it('should display "standard" priority for sandbox plan', () => { - setupProviderContext({ type: Plan.sandbox }) + setupProviderContext({ type: 'sandbox' }) render() @@ -744,7 +726,7 @@ describe('PriorityLabel Integration', () => { }) it('should display "priority" for professional plan with icon', () => { - setupProviderContext({ type: Plan.professional }) + setupProviderContext({ type: 'professional' }) const { container } = render() @@ -754,21 +736,13 @@ describe('PriorityLabel Integration', () => { }) it('should display "top-priority" for team plan with icon', () => { - setupProviderContext({ type: Plan.team }) + setupProviderContext({ type: 'team' }) const { container } = render() expect(screen.getByText(/plansCommon\.priority\.top-priority/i)).toBeInTheDocument() expect(container.querySelector('svg')).toBeInTheDocument() }) - - it('should display "top-priority" for enterprise plan', () => { - setupProviderContext({ type: Plan.enterprise }) - - render() - - expect(screen.getByText(/plansCommon\.priority\.top-priority/i)).toBeInTheDocument() - }) }) // ═══════════════════════════════════════════════════════════════════════════ @@ -785,7 +759,7 @@ describe('Usage Display Edge Cases', () => { describe('VectorSpace storage mode in PlanComp', () => { it('should show "< 50" for sandbox plan with low vector space usage', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { vectorSpace: 10 }, total: { vectorSpace: 50 }, }) @@ -798,7 +772,7 @@ describe('Usage Display Edge Cases', () => { it('should show indeterminate progress bar for usage below threshold', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { vectorSpace: 10 }, total: { vectorSpace: 50 }, }) @@ -811,7 +785,7 @@ describe('Usage Display Edge Cases', () => { it('should show actual usage for pro plan above threshold', () => { setupProviderContext({ - type: Plan.professional, + type: 'professional', usage: { vectorSpace: 1024 }, total: { vectorSpace: 5120 }, }) @@ -827,7 +801,7 @@ describe('Usage Display Edge Cases', () => { describe('Progress bar color reflects usage severity', () => { it('should show normal color for low usage percentage', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 1 }, total: { buildApps: 5 }, }) @@ -845,7 +819,7 @@ describe('Usage Display Edge Cases', () => { describe('Reset days integration', () => { it('should not show reset for sandbox trigger events (no reset_date)', () => { setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', total: { triggerEvents: 3000 }, reset: { triggerEvents: null }, }) @@ -861,7 +835,7 @@ describe('Usage Display Edge Cases', () => { it('should show reset for professional trigger events with reset date', () => { setupProviderContext({ - type: Plan.professional, + type: 'professional', total: { triggerEvents: 20000 }, reset: { triggerEvents: 14 }, }) @@ -888,7 +862,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from AppsFull upgrade button', async () => { const user = userEvent.setup() setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 5 }, total: { buildApps: 5 }, }) @@ -904,7 +878,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from VectorSpaceFull upgrade button', async () => { const user = userEvent.setup() setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { vectorSpace: 50 }, total: { vectorSpace: 50 }, }) @@ -920,7 +894,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from AnnotationFull upgrade button', async () => { const user = userEvent.setup() setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { annotatedResponse: 10 }, total: { annotatedResponse: 10 }, }) @@ -936,7 +910,7 @@ describe('Cross-Component Upgrade Flow', () => { it('should trigger pricing from TriggerEventsLimitModal through PlanUpgradeModal', async () => { const user = userEvent.setup() const onClose = vi.fn() - setupProviderContext({ type: Plan.professional }) + setupProviderContext({ type: 'professional' }) render( { it('should trigger pricing from AnnotationFullModal upgrade button', async () => { const user = userEvent.setup() setupProviderContext({ - type: Plan.sandbox, + type: 'sandbox', usage: { annotatedResponse: 10 }, total: { annotatedResponse: 10 }, }) diff --git a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx index 225d35acd4a..dd5d53ef0e0 100644 --- a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx +++ b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx @@ -7,7 +7,7 @@ * Covers plan comparison, downgrade prevention, monthly/yearly pricing, * and workspace manager permission enforcement. */ -import type { BasicPlan } from '@/app/components/billing/type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { toast, ToastHost } from '@langgenius/dify-ui/toast' import { cleanup, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -15,13 +15,12 @@ import * as React from 'react' import { ALL_PLANS } from '@/app/components/billing/config' import { PlanRange } from '@/app/components/billing/pricing/plan-switcher/plan-range-switcher' import CloudPlanItem from '@/app/components/billing/pricing/plans/cloud-plan-item' -import { Plan } from '@/app/components/billing/type' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' // ─── Mock state ────────────────────────────────────────────────────────────── let mockConsoleState: Record = {} -const mockFetchSubscriptionUrls = vi.fn() +const mockGetSubscription = vi.fn() const mockOpenAsyncWindow = vi.fn() // ─── Context mocks ─────────────────────────────────────────────────────────── @@ -31,10 +30,23 @@ vi.mock('@/context/workspace-state', async () => { return createWorkspaceStateModuleMock(() => mockConsoleState) }) -// ─── Service mocks ─────────────────────────────────────────────────────────── -vi.mock('@/service/billing', () => ({ - fetchSubscriptionUrls: (...args: unknown[]) => mockFetchSubscriptionUrls(...args), -})) +vi.mock('@/service/client', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + consoleClient: new Proxy(actual.consoleClient, { + get(target, prop, receiver) { + if (prop === 'billing') { + return { + invoices: target.billing.invoices, + subscription: { get: mockGetSubscription }, + } + } + return Reflect.get(target, prop, receiver) + }, + }), + } +}) vi.mock('@/hooks/use-async-window-open', () => ({ useAsyncWindowOpen: () => mockOpenAsyncWindow, @@ -56,15 +68,15 @@ const setupConsoleState = (overrides: Record = {}) => { } type RenderCloudPlanItemOptions = { - currentPlan?: BasicPlan - plan?: BasicPlan + currentPlan?: CloudPlan + plan?: CloudPlan planRange?: PlanRange canPay?: boolean } const renderCloudPlanItem = ({ - currentPlan = Plan.sandbox, - plan = Plan.professional, + currentPlan = 'sandbox', + plan = 'professional', planRange = PlanRange.monthly, canPay = true, }: RenderCloudPlanItemOptions = {}) => { @@ -87,32 +99,32 @@ describe('Cloud Plan Payment Flow', () => { cleanup() toast.dismiss() setupConsoleState() - mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://pay.example.com/checkout' }) + mockGetSubscription.mockResolvedValue({ url: 'https://pay.example.com/checkout' }) }) // ─── 1. Plan Display ──────────────────────────────────────────────────── describe('Plan display', () => { it('should render plan name and description', () => { - renderCloudPlanItem({ plan: Plan.professional }) + renderCloudPlanItem({ plan: 'professional' }) expect(screen.getByText(/plans\.professional\.name/i)).toBeInTheDocument() expect(screen.getByText(/plans\.professional\.description/i)).toBeInTheDocument() }) it('should show "Free" price for sandbox plan', () => { - renderCloudPlanItem({ plan: Plan.sandbox }) + renderCloudPlanItem({ plan: 'sandbox' }) expect(screen.getByText(/plansCommon\.free/i)).toBeInTheDocument() }) it('should show monthly price for paid plans', () => { - renderCloudPlanItem({ plan: Plan.professional, planRange: PlanRange.monthly }) + renderCloudPlanItem({ plan: 'professional', planRange: PlanRange.monthly }) expect(screen.getByText(`$${ALL_PLANS.professional.price}`)).toBeInTheDocument() }) it('should show yearly discounted price (10 months) and strikethrough original (12 months)', () => { - renderCloudPlanItem({ plan: Plan.professional, planRange: PlanRange.yearly }) + renderCloudPlanItem({ plan: 'professional', planRange: PlanRange.yearly }) const yearlyPrice = ALL_PLANS.professional.price * 10 const originalPrice = ALL_PLANS.professional.price * 12 @@ -122,17 +134,17 @@ describe('Cloud Plan Payment Flow', () => { }) it('should show "most popular" badge for professional plan', () => { - renderCloudPlanItem({ plan: Plan.professional }) + renderCloudPlanItem({ plan: 'professional' }) expect(screen.getByText(/plansCommon\.mostPopular/i)).toBeInTheDocument() }) it('should not show "most popular" badge for sandbox or team plans', () => { - const { unmount } = renderCloudPlanItem({ plan: Plan.sandbox }) + const { unmount } = renderCloudPlanItem({ plan: 'sandbox' }) expect(screen.queryByText(/plansCommon\.mostPopular/i)).not.toBeInTheDocument() unmount() - renderCloudPlanItem({ plan: Plan.team }) + renderCloudPlanItem({ plan: 'team' }) expect(screen.queryByText(/plansCommon\.mostPopular/i)).not.toBeInTheDocument() }) }) @@ -140,25 +152,25 @@ describe('Cloud Plan Payment Flow', () => { // ─── 2. Button Text Logic ─────────────────────────────────────────────── describe('Button text logic', () => { it('should show "Current Plan" when plan matches current plan', () => { - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: 'professional', plan: 'professional' }) expect(screen.getByText(/plansCommon\.currentPlan/i)).toBeInTheDocument() }) it('should show "Start for Free" for sandbox plan when not current', () => { - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.sandbox }) + renderCloudPlanItem({ currentPlan: 'professional', plan: 'sandbox' }) expect(screen.getByText(/plansCommon\.startForFree/i)).toBeInTheDocument() }) it('should show "Start Building" for professional plan when not current', () => { - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'professional' }) expect(screen.getByText(/plansCommon\.startBuilding/i)).toBeInTheDocument() }) it('should show "Get Started" for team plan when not current', () => { - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.team }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'team' }) expect(screen.getByText(/plansCommon\.getStarted/i)).toBeInTheDocument() }) @@ -167,30 +179,30 @@ describe('Cloud Plan Payment Flow', () => { // ─── 3. Downgrade Prevention ──────────────────────────────────────────── describe('Downgrade prevention', () => { it('should disable sandbox button when user is on professional plan (downgrade)', () => { - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.sandbox }) + renderCloudPlanItem({ currentPlan: 'professional', plan: 'sandbox' }) const button = getPlanButton('billing.plansCommon.startForFree') expect(button).toBeDisabled() }) it('should disable sandbox and professional buttons when user is on team plan', () => { - const { unmount } = renderCloudPlanItem({ currentPlan: Plan.team, plan: Plan.sandbox }) + const { unmount } = renderCloudPlanItem({ currentPlan: 'team', plan: 'sandbox' }) expect(getPlanButton('billing.plansCommon.startForFree')).toBeDisabled() unmount() - renderCloudPlanItem({ currentPlan: Plan.team, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: 'team', plan: 'professional' }) expect(getPlanButton('billing.plansCommon.startBuilding')).toBeDisabled() }) it('should not disable current paid plan button (for invoice management)', () => { - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: 'professional', plan: 'professional' }) const button = getPlanButton('billing.plansCommon.currentPlan') expect(button).not.toBeDisabled() }) it('should enable higher-tier plan buttons for upgrade', () => { - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.team }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'team' }) const button = getPlanButton('billing.plansCommon.getStarted') expect(button).not.toBeDisabled() @@ -199,12 +211,12 @@ describe('Cloud Plan Payment Flow', () => { // ─── 4. Payment URL Flow ──────────────────────────────────────────────── describe('Payment URL flow', () => { - it('should call fetchSubscriptionUrls with plan and "month" for monthly range', async () => { + it('should call get subscription with plan and "month" for monthly range', async () => { const user = userEvent.setup() // Simulate clicking on a professional plan button (user is on sandbox) renderCloudPlanItem({ - currentPlan: Plan.sandbox, - plan: Plan.professional, + currentPlan: 'sandbox', + plan: 'professional', planRange: PlanRange.monthly, }) @@ -212,15 +224,17 @@ describe('Cloud Plan Payment Flow', () => { await user.click(button) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'month') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'professional', interval: 'month' }, + }) }) }) - it('should call fetchSubscriptionUrls with plan and "year" for yearly range', async () => { + it('should call get subscription with plan and "year" for yearly range', async () => { const user = userEvent.setup() renderCloudPlanItem({ - currentPlan: Plan.sandbox, - plan: Plan.team, + currentPlan: 'sandbox', + plan: 'team', planRange: PlanRange.yearly, }) @@ -228,13 +242,15 @@ describe('Cloud Plan Payment Flow', () => { await user.click(button) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.team, 'year') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'team', interval: 'year' }, + }) }) }) it('should open invoice management for current paid plan', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.professional, plan: Plan.professional }) + renderCloudPlanItem({ currentPlan: 'professional', plan: 'professional' }) const button = getPlanButton('billing.plansCommon.currentPlan') await user.click(button) @@ -242,20 +258,20 @@ describe('Cloud Plan Payment Flow', () => { await waitFor(() => { expect(mockOpenAsyncWindow).toHaveBeenCalled() }) - // Should NOT call fetchSubscriptionUrls (invoice, not subscription) - expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + // Should NOT call get subscription (invoice, not subscription) + expect(mockGetSubscription).not.toHaveBeenCalled() }) it('should not do anything when clicking on sandbox free plan button', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.sandbox }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'sandbox' }) const button = getPlanButton('billing.plansCommon.currentPlan') await user.click(button) // Wait a tick and verify no actions were taken await waitFor(() => { - expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + expect(mockGetSubscription).not.toHaveBeenCalled() expect(mockOpenAsyncWindow).not.toHaveBeenCalled() }) }) @@ -265,19 +281,21 @@ describe('Cloud Plan Payment Flow', () => { describe('Payment capability', () => { it('should change plans when payment is allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional, canPay: true }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'professional', canPay: true }) const button = getPlanButton('billing.plansCommon.startBuilding') await user.click(button) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'month') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'professional', interval: 'month' }, + }) }) }) it('should block plan changes when payment is not allowed', async () => { const user = userEvent.setup() - renderCloudPlanItem({ currentPlan: Plan.sandbox, plan: Plan.professional, canPay: false }) + renderCloudPlanItem({ currentPlan: 'sandbox', plan: 'professional', canPay: false }) const button = getPlanButton('billing.plansCommon.startBuilding') await user.click(button) @@ -285,14 +303,14 @@ describe('Cloud Plan Payment Flow', () => { await waitFor(() => { expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) - expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + expect(mockGetSubscription).not.toHaveBeenCalled() }) it('should open billing portal when payment is allowed', async () => { const user = userEvent.setup() renderCloudPlanItem({ - currentPlan: Plan.professional, - plan: Plan.professional, + currentPlan: 'professional', + plan: 'professional', canPay: true, }) @@ -302,14 +320,14 @@ describe('Cloud Plan Payment Flow', () => { await waitFor(() => { expect(mockOpenAsyncWindow).toHaveBeenCalled() }) - expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + expect(mockGetSubscription).not.toHaveBeenCalled() }) it('should block billing portal access when payment is not allowed', async () => { const user = userEvent.setup() renderCloudPlanItem({ - currentPlan: Plan.professional, - plan: Plan.professional, + currentPlan: 'professional', + plan: 'professional', canPay: false, }) diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index c4d33fb45f7..e9f79a71697 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -5,7 +5,6 @@ import { cleanup, screen } from '@testing-library/react' import * as React from 'react' import { defaultPlan } from '@/app/components/billing/config' import PlanComp from '@/app/components/billing/plan' -import { Plan } from '@/app/components/billing/type' import { consoleQuery } from '@/service/client' import { createConsoleQueryClient, @@ -161,7 +160,7 @@ describe('Education Verification Flow', () => { // ─── 2. Education + Upgrade Coexistence ───────────────────────────────── describe('Education and upgrade button coexistence', () => { it('should show both education verify and upgrade buttons for sandbox user', () => { - setupContexts({ type: Plan.sandbox }, { enableEducationPlan: true }) + setupContexts({ type: 'sandbox' }, { enableEducationPlan: true }) render() @@ -169,18 +168,8 @@ describe('Education Verification Flow', () => { expect(screen.getByText(/upgradeBtn\.encourageShort/i)).toBeInTheDocument() }) - it('should not show upgrade button for enterprise plan', () => { - setupContexts({ type: Plan.enterprise }, { enableEducationPlan: true }) - - render() - - expect(screen.getByText(/toVerified/i)).toBeInTheDocument() - expect(screen.queryByText(/upgradeBtn\.encourageShort/i)).not.toBeInTheDocument() - expect(screen.queryByText(/upgradeBtn\.plain/i)).not.toBeInTheDocument() - }) - it('should show team plan with plain upgrade button and education button', () => { - setupContexts({ type: Plan.team }, { enableEducationPlan: true }) + setupContexts({ type: 'team' }, { enableEducationPlan: true }) render() diff --git a/web/__tests__/billing/pricing-modal-flow.test.tsx b/web/__tests__/billing/pricing-modal-flow.test.tsx index df4ee8a0db6..1ae301976b9 100644 --- a/web/__tests__/billing/pricing-modal-flow.test.tsx +++ b/web/__tests__/billing/pricing-modal-flow.test.tsx @@ -13,7 +13,6 @@ import userEvent from '@testing-library/user-event' import * as React from 'react' import { ALL_PLANS } from '@/app/components/billing/config' import Pricing from '@/app/components/billing/pricing' -import { Plan } from '@/app/components/billing/type' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render as renderWithConsoleState } from '@/test/console/render' @@ -21,7 +20,7 @@ import { render as renderWithConsoleState } from '@/test/console/render' let mockProviderCtx: Record = {} let mockConsoleState: Record = {} let mockEducationStatus = { is_student: false, allow_refresh: false, expire_at: null } -const mockFetchSubscriptionUrls = vi.hoisted(() => vi.fn()) +const mockGetSubscription = vi.hoisted(() => vi.fn()) const render = (ui: React.ReactElement) => { const { wrapper } = createConsoleQueryWrapper({ @@ -46,11 +45,6 @@ vi.mock('@/context/i18n', () => ({ useGetPricingPageLanguage: () => 'en', })) -// ─── Service mocks ─────────────────────────────────────────────────────────── -vi.mock('@/service/billing', () => ({ - fetchSubscriptionUrls: (...args: unknown[]) => mockFetchSubscriptionUrls(...args), -})) - vi.mock('@/service/client', async (importOriginal) => { const actual = await importOriginal() return { @@ -62,6 +56,7 @@ vi.mock('@/service/client', async (importOriginal) => { invoices: { get: vi.fn().mockResolvedValue({ url: 'https://invoice.example.com' }), }, + subscription: { get: mockGetSubscription }, } } return Reflect.get(target, prop, receiver) @@ -104,7 +99,7 @@ vi.mock('@/app/components/billing/pricing/plans/self-hosted-plan-item/list', () // ─── Helpers ───────────────────────────────────────────────────────────────── const defaultPlanData = { - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 1, teamMembers: 1, @@ -151,7 +146,7 @@ describe('Pricing Modal Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() - mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://pay.example.com' }) + mockGetSubscription.mockResolvedValue({ url: 'https://pay.example.com' }) setupContexts() }) @@ -296,7 +291,9 @@ describe('Pricing Modal Flow', () => { await user.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'month') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'professional', interval: 'month' }, + }) }) }) @@ -317,7 +314,9 @@ describe('Pricing Modal Flow', () => { await user.click(screen.getByRole('button', { name: 'education.useEducationDiscount' })) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'year') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'professional', interval: 'year' }, + }) }) }) @@ -335,19 +334,19 @@ describe('Pricing Modal Flow', () => { await user.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) await waitFor(() => { - expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() + expect(mockGetSubscription).not.toHaveBeenCalled() }) }) it('should show "Current Plan" for the current plan (sandbox)', () => { - setupContexts({ type: Plan.sandbox }) + setupContexts({ type: 'sandbox' }) render() expect(screen.getByText(/plansCommon\.currentPlan/i)).toBeInTheDocument() }) it('should show specific button text for non-current plans', () => { - setupContexts({ type: Plan.sandbox }) + setupContexts({ type: 'sandbox' }) render() // Professional button text @@ -355,14 +354,6 @@ describe('Pricing Modal Flow', () => { // Team button text expect(screen.getByText(/plansCommon\.getStarted/i)).toBeInTheDocument() }) - - it('should mark sandbox as "Current Plan" for professional user (enterprise normalized to team)', () => { - setupContexts({ type: Plan.enterprise }) - render() - - // Enterprise is normalized to team for display, so team is "Current Plan" - expect(screen.getByText(/plansCommon\.currentPlan/i)).toBeInTheDocument() - }) }) // ─── 5. Self-Hosted Plan Details ───────────────────────────────────────── diff --git a/web/__tests__/billing/self-hosted-plan-flow.test.tsx b/web/__tests__/billing/self-hosted-plan-flow.test.tsx index eab27209e1e..f76955b4903 100644 --- a/web/__tests__/billing/self-hosted-plan-flow.test.tsx +++ b/web/__tests__/billing/self-hosted-plan-flow.test.tsx @@ -1,3 +1,4 @@ +import type { SelfHostedPlanOption } from '@/app/components/billing/pricing/plans/self-hosted-plan-item/types' /** * Integration test: Self-Hosted Plan Flow * @@ -15,7 +16,6 @@ import { getWithPremiumUrl, } from '@/app/components/billing/config' import SelfHostedPlanItem from '@/app/components/billing/pricing/plans/self-hosted-plan-item' -import { SelfHostedPlan } from '@/app/components/billing/type' import { render } from '@/test/console/render' const originalLocation = window.location @@ -39,7 +39,7 @@ vi.mock('@/app/components/billing/pricing/plans/self-hosted-plan-item/list', () ), })) -const renderSelfHostedPlanItem = (plan: SelfHostedPlan) => { +const renderSelfHostedPlanItem = (plan: SelfHostedPlanOption) => { return render() } @@ -74,14 +74,14 @@ describe('Self-Hosted Plan Flow', () => { // ─── 1. Plan Rendering ────────────────────────────────────────────────── describe('Plan rendering', () => { it('should render community plan with name and description', () => { - renderSelfHostedPlanItem(SelfHostedPlan.community) + renderSelfHostedPlanItem('community') expect(screen.getByText(/plans\.community\.name/i)).toBeInTheDocument() expect(screen.getByText(/plans\.community\.description/i)).toBeInTheDocument() }) it('should render premium plan with cloud provider icons', () => { - renderSelfHostedPlanItem(SelfHostedPlan.premium) + renderSelfHostedPlanItem('premium') expect(screen.getByText(/plans\.premium\.name/i)).toBeInTheDocument() expect(screen.getByTestId('icon-azure')).toBeInTheDocument() @@ -89,39 +89,39 @@ describe('Self-Hosted Plan Flow', () => { }) it('should render enterprise plan without cloud provider icons', () => { - renderSelfHostedPlanItem(SelfHostedPlan.enterprise) + renderSelfHostedPlanItem('enterprise') expect(screen.getByText(/plans\.enterprise\.name/i)).toBeInTheDocument() expect(screen.queryByTestId('icon-azure')).not.toBeInTheDocument() }) it('should not show price tip for community (free) plan', () => { - renderSelfHostedPlanItem(SelfHostedPlan.community) + renderSelfHostedPlanItem('community') expect(screen.queryByText(/plans\.community\.priceTip/i)).not.toBeInTheDocument() }) it('should show price tip for premium plan', () => { - renderSelfHostedPlanItem(SelfHostedPlan.premium) + renderSelfHostedPlanItem('premium') expect(screen.getByText(/plans\.premium\.priceTip/i)).toBeInTheDocument() }) it('should render features list for each plan', () => { - const { unmount: unmount1 } = renderSelfHostedPlanItem(SelfHostedPlan.community) + const { unmount: unmount1 } = renderSelfHostedPlanItem('community') expect(screen.getByTestId('self-hosted-list-community')).toBeInTheDocument() unmount1() - const { unmount: unmount2 } = renderSelfHostedPlanItem(SelfHostedPlan.premium) + const { unmount: unmount2 } = renderSelfHostedPlanItem('premium') expect(screen.getByTestId('self-hosted-list-premium')).toBeInTheDocument() unmount2() - renderSelfHostedPlanItem(SelfHostedPlan.enterprise) + renderSelfHostedPlanItem('enterprise') expect(screen.getByTestId('self-hosted-list-enterprise')).toBeInTheDocument() }) it('should show AWS marketplace icon for premium plan button', () => { - renderSelfHostedPlanItem(SelfHostedPlan.premium) + renderSelfHostedPlanItem('premium') expect(screen.getByTestId('icon-aws-light')).toBeInTheDocument() }) @@ -131,7 +131,7 @@ describe('Self-Hosted Plan Flow', () => { describe('Navigation flow', () => { it('should redirect to GitHub when clicking community plan button', async () => { const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.community) + renderSelfHostedPlanItem('community') const button = screen.getByRole('button') await user.click(button) @@ -141,7 +141,7 @@ describe('Self-Hosted Plan Flow', () => { it('should redirect to AWS Marketplace when clicking premium plan button', async () => { const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.premium) + renderSelfHostedPlanItem('premium') const button = screen.getByRole('button') await user.click(button) @@ -151,7 +151,7 @@ describe('Self-Hosted Plan Flow', () => { it('should redirect to Typeform when clicking enterprise plan button', async () => { const user = userEvent.setup() - renderSelfHostedPlanItem(SelfHostedPlan.enterprise) + renderSelfHostedPlanItem('enterprise') const button = screen.getByRole('button') await user.click(button) diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx index a057f2e6e22..ecd3fd442ee 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx @@ -127,7 +127,6 @@ function createMockProviderContext( updateModelList: vi.fn(), onPlanInfoChanged: vi.fn(), refreshModelProviders: vi.fn(), - refreshLicenseLimit: vi.fn(), ...overrides, } as ProviderContextState } diff --git a/web/app/components/app/log/__tests__/archived-logs-notice.spec.tsx b/web/app/components/app/log/__tests__/archived-logs-notice.spec.tsx index 3e345357417..b179694e5ab 100644 --- a/web/app/components/app/log/__tests__/archived-logs-notice.spec.tsx +++ b/web/app/components/app/log/__tests__/archived-logs-notice.spec.tsx @@ -1,8 +1,8 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import { defaultPlan } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { useProviderContext } from '@/context/provider-context' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' @@ -32,7 +32,7 @@ vi.mock('nuqs', async (importOriginal) => { const mockUseProviderContext = vi.mocked(useProviderContext) -function mockProviderPlan(planType: Plan) { +function mockProviderPlan(planType: CloudPlan) { mockUseProviderContext.mockReturnValue( createMockProviderContextValue({ enableBilling: true, @@ -54,7 +54,7 @@ describe('ArchivedLogsNotice', () => { beforeEach(() => { vi.clearAllMocks() - mockProviderPlan(Plan.professional) + mockProviderPlan('professional') }) it('should show an accessible notice for paid workspace managers', async () => { @@ -71,7 +71,7 @@ describe('ArchivedLogsNotice', () => { }) it('should not show notice for sandbox workspaces', () => { - mockProviderPlan(Plan.sandbox) + mockProviderPlan('sandbox') renderNotice() diff --git a/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx b/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx index 13e72de8a16..b94f5ad2fd0 100644 --- a/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx +++ b/web/app/components/app/log/__tests__/retention-upgrade-notice.spec.tsx @@ -1,9 +1,9 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import { defaultPlan } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { createConsoleQueryWrapper } from '@/test/console/query-data' @@ -36,12 +36,12 @@ describe('RetentionUpgradeNotice', () => { enableBilling = true, isFetchedPlan = true, isFetchedPlanInfo = true, - planType = Plan.sandbox, + planType = 'sandbox', }: { enableBilling?: boolean isFetchedPlan?: boolean isFetchedPlanInfo?: boolean - planType?: Plan + planType?: CloudPlan } = {}) { mockUseProviderContext.mockReturnValue( createMockProviderContextValue({ @@ -89,12 +89,12 @@ describe('RetentionUpgradeNotice', () => { it.each([ { name: 'paid Cloud workspaces', - provider: { planType: Plan.professional }, + provider: { planType: 'professional' }, deploymentEdition: 'CLOUD', }, { name: 'self-hosted sandbox workspaces', - provider: { planType: Plan.sandbox }, + provider: { planType: 'sandbox' }, deploymentEdition: 'COMMUNITY', }, { diff --git a/web/app/components/app/log/archived-logs-notice.tsx b/web/app/components/app/log/archived-logs-notice.tsx index 2792e3eade5..4053351b722 100644 --- a/web/app/components/app/log/archived-logs-notice.tsx +++ b/web/app/components/app/log/archived-logs-notice.tsx @@ -5,7 +5,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useQueryState } from 'nuqs' import { useTranslation } from 'react-i18next' -import { Plan } from '@/app/components/billing/type' import { settingsQueryParamName, settingsQueryParser, @@ -28,7 +27,7 @@ export function ArchivedLogsNotice() { deploymentEdition !== 'CLOUD' || !isCurrentWorkspaceManager || !enableBilling || - plan.type === Plan.sandbox + plan.type === 'sandbox' ) return null diff --git a/web/app/components/app/log/cloud-sandbox-retention.ts b/web/app/components/app/log/cloud-sandbox-retention.ts index c7f7db14fc1..29bc15398b7 100644 --- a/web/app/components/app/log/cloud-sandbox-retention.ts +++ b/web/app/components/app/log/cloud-sandbox-retention.ts @@ -1,7 +1,6 @@ 'use client' import { useSuspenseQuery } from '@tanstack/react-query' -import { Plan } from '@/app/components/billing/type' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' @@ -50,5 +49,5 @@ export function useCloudSandboxPlanStatus(): CloudSandboxPlanState { if (!enableBilling) return 'unrestricted' if (!isFetchedPlan) return 'pending' - return plan.type === Plan.sandbox ? 'sandbox' : 'unrestricted' + return plan.type === 'sandbox' ? 'sandbox' : 'unrestricted' } diff --git a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx index 16e26a4200d..5b6156a642c 100644 --- a/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx +++ b/web/app/components/app/overview/apikey-info-panel/__tests__/test-utils.tsx @@ -53,17 +53,8 @@ const defaultProviderContext = { onPlanInfoChanged: noop, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, - datasetOperatorEnabled: false, enableEducationPlan: false, - isEducationWorkspace: false, webappCopyrightEnabled: false, - licenseLimit: { - workspace_members: { - size: 0, - limit: 0, - }, - }, - refreshLicenseLimit: noop, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, diff --git a/web/app/components/app/overview/settings/__tests__/index.spec.tsx b/web/app/components/app/overview/settings/__tests__/index.spec.tsx index 5298bb0a4a2..702f344237f 100644 --- a/web/app/components/app/overview/settings/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/settings/__tests__/index.spec.tsx @@ -4,7 +4,6 @@ import type { ProviderContextState } from '@/context/provider-context' import type { AppDetailResponse } from '@/models/app' import type { AppSSO } from '@/types/app' import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { Plan } from '@/app/components/billing/type' import { baseProviderContextValue } from '@/context/provider-context' import { AppModeEnum } from '@/types/app' import SettingsModal from '../index' @@ -142,7 +141,7 @@ describe('SettingsModal', () => { enableBilling: true, plan: { ...baseProviderContextValue.plan, - type: Plan.professional, + type: 'professional', }, webappCopyrightEnabled: true, }) @@ -366,7 +365,7 @@ describe('SettingsModal', () => { enableBilling: true, plan: { ...baseProviderContextValue.plan, - type: Plan.sandbox, + type: 'sandbox', }, webappCopyrightEnabled: true, }) @@ -401,7 +400,7 @@ describe('SettingsModal', () => { enableBilling: false, plan: { ...baseProviderContextValue.plan, - type: Plan.sandbox, + type: 'sandbox', }, webappCopyrightEnabled: false, }) @@ -429,7 +428,7 @@ describe('SettingsModal', () => { enableBilling: true, plan: { ...baseProviderContextValue.plan, - type: Plan.sandbox, + type: 'sandbox', }, webappCopyrightEnabled: false, }) @@ -446,7 +445,7 @@ describe('SettingsModal', () => { enableBilling: true, plan: { ...baseProviderContextValue.plan, - type: Plan.professional, + type: 'professional', }, webappCopyrightEnabled: true, }) diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index adca1911fe2..61ea0ff3f5f 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -34,7 +34,6 @@ import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' import Divider from '@/app/components/base/divider' import { PremiumBadgeButton } from '@/app/components/base/premium-badge' -import { Plan } from '@/app/components/billing/type' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { languages } from '@/i18n-config/language' @@ -205,7 +204,7 @@ const SettingsModal: FC = ({ const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext() const { setShowPricingModal } = useModalContext() - const isCloudSandboxPlan = enableBilling && plan.type === Plan.sandbox + const isCloudSandboxPlan = enableBilling && plan.type === 'sandbox' const selectedLanguage = LANGUAGE_OPTIONS.find((item) => item.value === language) const inputPlaceholderLabelId = React.useId() const inputPlaceholderDescriptionId = React.useId() diff --git a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx index 0791aceff2c..3868b51505e 100644 --- a/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/__tests__/index.spec.tsx @@ -3,7 +3,6 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { useStore as useAppStore } from '@/app/components/app/store' -import { Plan } from '@/app/components/billing/type' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { AppModeEnum } from '@/types/app' import SwitchAppModal from '../index' @@ -40,7 +39,7 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { let mockEnableBilling = false let mockPlan = { - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 0, teamMembers: 0, @@ -155,7 +154,7 @@ describe('SwitchAppModal', () => { useAppStore.setState({ setAppDetail: setAppDetailSpy as typeof originalSetAppDetail }) mockEnableBilling = false mockPlan = { - type: Plan.sandbox, + type: 'sandbox', usage: { buildApps: 0, teamMembers: 0, diff --git a/web/app/components/billing/apps-full-in-dialog/index.tsx b/web/app/components/billing/apps-full-in-dialog/index.tsx index 2aa458683c1..6896213921d 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.tsx +++ b/web/app/components/billing/apps-full-in-dialog/index.tsx @@ -7,7 +7,6 @@ import { Meter, MeterIndicator, MeterTrack } from '@langgenius/dify-ui/meter' import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' -import { Plan } from '@/app/components/billing/type' import { mailToSupport } from '@/app/components/header/utils/util' import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' @@ -24,7 +23,7 @@ const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) = currentVersion: data.meta.currentVersion, }), }) - const isTeam = plan.type === Plan.team + const isTeam = plan.type === 'team' const usage = plan.usage.buildApps const total = plan.total.buildApps const percent = total > 0 ? (usage / total) * 100 : 0 @@ -58,10 +57,10 @@ const AppsFull: FC<{ loc: string; className?: string }> = ({ loc, className }) = )} - {(plan.type === Plan.sandbox || plan.type === Plan.professional) && ( + {(plan.type === 'sandbox' || plan.type === 'professional') && ( )} - {plan.type !== Plan.sandbox && plan.type !== Plan.professional && ( + {plan.type !== 'sandbox' && plan.type !== 'professional' && ( )} - {isCloudEdition && !isEnterprisePlan && ( - + {isCloudEdition && ( + )} diff --git a/web/app/components/billing/pricing/__tests__/dialog.spec.tsx b/web/app/components/billing/pricing/__tests__/dialog.spec.tsx index 80ce4126a53..61befc0ea9c 100644 --- a/web/app/components/billing/pricing/__tests__/dialog.spec.tsx +++ b/web/app/components/billing/pricing/__tests__/dialog.spec.tsx @@ -6,7 +6,6 @@ import { useGetPricingPageLanguage } from '@/context/i18n' import { useProviderContext } from '@/context/provider-context' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' -import { Plan } from '../../type' import Pricing from '../index' let mockConsoleState: Record = {} @@ -63,7 +62,7 @@ describe('Pricing dialog lifecycle', () => { ;(useProviderContext as Mock).mockReturnValue({ enableEducationPlan: false, plan: { - type: Plan.sandbox, + type: 'sandbox', usage: buildUsage(), total: buildUsage(), }, diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/button.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/button.tsx index ea6abd74ac4..f1fa81c93ae 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/button.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/button.tsx @@ -1,22 +1,21 @@ -import type { BasicPlan } from '../../../type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' -import { Plan } from '../../../type' const BUTTON_CLASSNAME = { - [Plan.sandbox]: { + sandbox: { btnClassname: 'bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover text-text-primary', btnDisabledClassname: 'bg-components-button-tertiary-bg-disabled hover:bg-components-button-tertiary-bg-disabled text-text-disabled', }, - [Plan.professional]: { + professional: { btnClassname: 'bg-saas-dify-blue-static hover:bg-saas-dify-blue-static-hover text-text-primary-on-surface', btnDisabledClassname: 'bg-components-button-tertiary-bg-disabled hover:bg-components-button-tertiary-bg-disabled text-text-disabled', }, - [Plan.team]: { + team: { btnClassname: 'bg-saas-background-inverted hover:bg-saas-background-inverted-hover text-background-default', btnDisabledClassname: @@ -25,7 +24,7 @@ const BUTTON_CLASSNAME = { } type ButtonProps = { - plan: BasicPlan + plan: CloudPlan isPlanDisabled: boolean btnText: string handleGetPayUrl: () => void diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 75f15d6047f..cb4bd5a6907 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -1,6 +1,6 @@ 'use client' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { FC } from 'react' -import type { BasicPlan } from '../../../type' import { Button } from '@langgenius/dify-ui/button' import { Dialog, @@ -16,25 +16,23 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { fetchSubscriptionUrls } from '@/service/billing' import { consoleClient, consoleQuery } from '@/service/client' import { ALL_PLANS } from '../../../config' import { useEducationDiscount } from '../../../hooks/use-education-discount' -import { Plan } from '../../../type' import { Professional, Sandbox, Team } from '../../assets' import { PlanRange } from '../../plan-switcher/plan-range-switcher' import PlanButton from './button' import List from './list' const ICON_MAP = { - [Plan.sandbox]: , - [Plan.professional]: , - [Plan.team]: , + sandbox: , + professional: , + team: , } type CloudPlanItemProps = { - currentPlan: BasicPlan - plan: BasicPlan + currentPlan: CloudPlan + plan: CloudPlan planRange: PlanRange canPay: boolean } @@ -43,8 +41,8 @@ const CloudPlanItem: FC = ({ plan, currentPlan, planRange, c const { t } = useTranslation() const [loading, setLoading] = React.useState(false) const i18nPrefix = `plans.${plan}` as const - const isFreePlan = plan === Plan.sandbox - const isMostPopularPlan = plan === Plan.professional + const isFreePlan = plan === 'sandbox' + const isMostPopularPlan = plan === 'professional' const planInfo = ALL_PLANS[plan] const isYear = planRange === PlanRange.yearly const isCurrent = plan === currentPlan @@ -58,7 +56,7 @@ const CloudPlanItem: FC = ({ plan, currentPlan, planRange, c }), ) const isEducationDiscountMode = enableEducationPlan && isEducationAccount - const isEducationDiscountSupportedPlan = plan === Plan.professional && isYear + const isEducationDiscountSupportedPlan = plan === 'professional' && isYear const educationDiscountWarningText = canPay && isEducationDiscountMode && !isFreePlan && !isEducationDiscountSupportedPlan ? t(($) => $.planNotSupportEducationDiscount, { ns: 'education' }) @@ -74,9 +72,9 @@ const CloudPlanItem: FC = ({ plan, currentPlan, planRange, c if (isCurrent) return t(($) => $['plansCommon.currentPlan'], { ns: 'billing' }) return { - [Plan.sandbox]: t(($) => $['plansCommon.startForFree'], { ns: 'billing' }), - [Plan.professional]: t(($) => $['plansCommon.startBuilding'], { ns: 'billing' }), - [Plan.team]: t(($) => $['plansCommon.getStarted'], { ns: 'billing' }), + sandbox: t(($) => $['plansCommon.startForFree'], { ns: 'billing' }), + professional: t(($) => $['plansCommon.startBuilding'], { ns: 'billing' }), + team: t(($) => $['plansCommon.getStarted'], { ns: 'billing' }), }[plan] }, [canPay, isCurrent, isEducationDiscountMode, isEducationDiscountSupportedPlan, plan, t]) @@ -120,7 +118,9 @@ const CloudPlanItem: FC = ({ plan, currentPlan, planRange, c return } - const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month') + const res = await consoleClient.billing.subscription.get({ + query: { plan, interval: isYear ? 'year' : 'month' }, + }) // Adb Block additional tracking block the gtag, so we need to redirect directly window.location.href = res.url } finally { diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx index 7fae5da17a9..202c3faf096 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/index.tsx @@ -1,18 +1,17 @@ -import type { BasicPlan } from '../../../../type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import * as React from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { ALL_PLANS, NUM_INFINITE } from '../../../../config' -import { Plan } from '../../../../type' import Item from './item' type ListProps = { - plan: BasicPlan + plan: CloudPlan } const List = ({ plan }: ListProps) => { const { t } = useTranslation() - const isFreePlan = plan === Plan.sandbox + const isFreePlan = plan === 'sandbox' const planInfo = ALL_PLANS[plan] return ( @@ -78,7 +77,7 @@ const List = ({ plan }: ListProps) => { label={ planInfo.triggerEvents === NUM_INFINITE ? t(($) => $['plansCommon.triggerEvents.unlimited'], { ns: 'billing' }) - : plan === Plan.sandbox + : plan === 'sandbox' ? t(($) => $['plansCommon.triggerEvents.sandbox'], { ns: 'billing', count: planInfo.triggerEvents, @@ -92,16 +91,16 @@ const List = ({ plan }: ListProps) => { /> $['plansCommon.startNodes.limited'], { ns: 'billing', count: 2 }) : t(($) => $['plansCommon.startNodes.unlimited'], { ns: 'billing' }) } /> $['plansCommon.workflowExecution.standard'], { ns: 'billing' }) - : plan === Plan.professional + : plan === 'professional' ? t(($) => $['plansCommon.workflowExecution.faster'], { ns: 'billing' }) : t(($) => $['plansCommon.workflowExecution.priority'], { ns: 'billing' }) } diff --git a/web/app/components/billing/pricing/plans/index.tsx b/web/app/components/billing/pricing/plans/index.tsx index 10c5fabb9dc..05729710901 100644 --- a/web/app/components/billing/pricing/plans/index.tsx +++ b/web/app/components/billing/pricing/plans/index.tsx @@ -1,13 +1,13 @@ -import type { BasicPlan, UsagePlanInfo } from '../../type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import type { UsagePlanInfo } from '../../type' import type { PlanRange } from '../plan-switcher/plan-range-switcher' import Divider from '@/app/components/base/divider' -import { Plan, SelfHostedPlan } from '../../type' import CloudPlanItem from './cloud-plan-item' import SelfHostedPlanItem from './self-hosted-plan-item' type PlansProps = { plan: { - type: Plan + type: CloudPlan usage: UsagePlanInfo total: UsagePlanInfo } @@ -17,7 +17,7 @@ type PlansProps = { } const Plans = ({ plan, currentPlan, planRange, canPay }: PlansProps) => { - const currentPlanType: BasicPlan = plan.type === Plan.enterprise ? Plan.team : plan.type + const currentPlanType = plan.type return (
@@ -25,21 +25,21 @@ const Plans = ({ plan, currentPlan, planRange, canPay }: PlansProps) => { <> @@ -47,11 +47,11 @@ const Plans = ({ plan, currentPlan, planRange, canPay }: PlansProps) => { )} {currentPlan === 'self' && ( <> - + - + - + )}
diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx index 176cedc3535..f3db901fad0 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx @@ -1,3 +1,4 @@ +import type { SelfHostedPlanOption } from './types' import { cn } from '@langgenius/dify-ui/cn' import { RiArrowRightLine } from '@remixicon/react' import * as React from 'react' @@ -9,19 +10,18 @@ import { } from '@/app/components/base/icons/src/public/billing' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' -import { SelfHostedPlan } from '../../../type' const BUTTON_CLASSNAME = { - [SelfHostedPlan.community]: + community: 'text-text-primary bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover', - [SelfHostedPlan.premium]: + premium: 'text-background-default bg-saas-background-inverted hover:bg-saas-background-inverted-hover', - [SelfHostedPlan.enterprise]: + enterprise: 'text-text-primary-on-surface bg-saas-dify-blue-static hover:bg-saas-dify-blue-static-hover', } type ButtonProps = { - plan: SelfHostedPlan + plan: SelfHostedPlanOption handleGetPayUrl: () => void } @@ -29,7 +29,7 @@ const Button = ({ plan, handleGetPayUrl }: ButtonProps) => { const { t } = useTranslation() const { theme } = useTheme() const i18nPrefix = `plans.${plan}` as const - const isPremiumPlan = plan === SelfHostedPlan.premium + const isPremiumPlan = plan === 'premium' const AwsMarketplace = useMemo(() => { return theme === Theme.light ? AwsMarketplaceLight : AwsMarketplaceDark }, [theme]) diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx index 14bb1dc8dd3..a0c4d1321d1 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx @@ -1,23 +1,23 @@ 'use client' import type { FC } from 'react' +import type { SelfHostedPlanOption } from './types' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Azure, GoogleCloud } from '@/app/components/base/icons/src/public/billing' import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../config' -import { SelfHostedPlan } from '../../../type' import { Community, Enterprise, EnterpriseNoise, Premium, PremiumNoise } from '../../assets' import Button from './button' import List from './list' const STYLE_MAP = { - [SelfHostedPlan.community]: { + community: { icon: , bg: '', noise: null, }, - [SelfHostedPlan.premium]: { + premium: { icon: , bg: 'bg-billing-plan-card-premium-bg opacity-10', noise: ( @@ -26,7 +26,7 @@ const STYLE_MAP = {
), }, - [SelfHostedPlan.enterprise]: { + enterprise: { icon: , bg: 'bg-billing-plan-card-enterprise-bg opacity-10', noise: ( @@ -38,15 +38,15 @@ const STYLE_MAP = { } type SelfHostedPlanItemProps = { - plan: SelfHostedPlan + plan: SelfHostedPlanOption } const SelfHostedPlanItem: FC = ({ plan }) => { const { t } = useTranslation() const i18nPrefix = `plans.${plan}` as const - const isFreePlan = plan === SelfHostedPlan.community - const isPremiumPlan = plan === SelfHostedPlan.premium - const isEnterprisePlan = plan === SelfHostedPlan.enterprise + const isFreePlan = plan === 'community' + const isPremiumPlan = plan === 'premium' + const isEnterprisePlan = plan === 'enterprise' const handleGetPayUrl = useCallback(() => { if (isFreePlan) { diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx index 257d702c38e..189531ce63c 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx @@ -1,10 +1,10 @@ -import type { SelfHostedPlan } from '@/app/components/billing/type' +import type { SelfHostedPlanOption } from '../types' import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import Item from './item' type ListProps = { - plan: SelfHostedPlan + plan: SelfHostedPlanOption } const List = ({ plan }: ListProps) => { diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/types.ts b/web/app/components/billing/pricing/plans/self-hosted-plan-item/types.ts new file mode 100644 index 00000000000..55ae69190e6 --- /dev/null +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/types.ts @@ -0,0 +1 @@ +export type SelfHostedPlanOption = 'community' | 'premium' | 'enterprise' diff --git a/web/app/components/billing/priority-label/index.tsx b/web/app/components/billing/priority-label/index.tsx index 0e7be9e601c..e50a4553fc9 100644 --- a/web/app/components/billing/priority-label/index.tsx +++ b/web/app/components/billing/priority-label/index.tsx @@ -4,7 +4,6 @@ import { RiAedFill } from '@remixicon/react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' -import { DocumentProcessingPriority, Plan } from '../type' type PriorityLabelProps = { className?: string @@ -15,14 +14,13 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { const { plan } = useProviderContext() const priority = useMemo(() => { - if (plan.type === Plan.sandbox) return DocumentProcessingPriority.standard + if (plan.type === 'sandbox') return 'standard' - if (plan.type === Plan.professional) return DocumentProcessingPriority.priority + if (plan.type === 'professional') return 'priority' - if (plan.type === Plan.team || plan.type === Plan.enterprise) - return DocumentProcessingPriority.topPriority + if (plan.type === 'team') return 'top-priority' - return DocumentProcessingPriority.standard + return 'standard' }, [plan]) return ( @@ -37,9 +35,9 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { /> } > - {(plan.type === Plan.professional || - plan.type === Plan.team || - plan.type === Plan.enterprise) && } + {(plan.type === 'professional' || plan.type === 'team') && ( + + )} {t(($) => $[`plansCommon.priority.${priority}`], { ns: 'billing' })} @@ -47,7 +45,7 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { {t(($) => $['plansCommon.documentProcessingPriority'], { ns: 'billing' })}:{' '} {t(($) => $[`plansCommon.priority.${priority}`], { ns: 'billing' })} - {priority !== DocumentProcessingPriority.topPriority && ( + {priority !== 'top-priority' && (
{t(($) => $['plansCommon.documentProcessingPriorityTip'], { ns: 'billing' })}
diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts index 214ce5605bd..22e62c02fa0 100644 --- a/web/app/components/billing/type.ts +++ b/web/app/components/billing/type.ts @@ -1,16 +1,4 @@ -export enum Plan { - sandbox = 'sandbox', - professional = 'professional', - team = 'team', - enterprise = 'enterprise', -} -export enum Priority { - standard = 'standard', - priority = 'priority', - topPriority = 'top-priority', -} - -export type BasicPlan = Plan.sandbox | Plan.professional | Plan.team +type DocumentProcessingPriority = 'standard' | 'priority' | 'top-priority' export type PlanInfo = { level: number @@ -24,92 +12,24 @@ export type PlanInfo = { documentsUploadQuota: number documentsRequestQuota: number apiRateLimit: number - documentProcessingPriority: Priority + documentProcessingPriority: DocumentProcessingPriority logHistory: number messageRequest: number triggerEvents: number annotatedResponse: number } -export enum SelfHostedPlan { - community = 'community', - premium = 'premium', - enterprise = 'enterprise', +export type UsagePlanInfo = { + buildApps: number + teamMembers: number + annotatedResponse: number + documentsUploadQuota: number + apiRateLimit: number + triggerEvents: number + vectorSpace: number } -export type UsagePlanInfo = Pick< - PlanInfo, - | 'buildApps' - | 'teamMembers' - | 'annotatedResponse' - | 'documentsUploadQuota' - | 'apiRateLimit' - | 'triggerEvents' -> & { vectorSpace: number } - export type UsageResetInfo = { apiRateLimit?: number | null triggerEvents?: number | null } - -export type BillingQuota = { - usage: number - limit: number - reset_date?: number | null -} - -export enum DocumentProcessingPriority { - standard = 'standard', - priority = 'priority', - topPriority = 'top-priority', -} - -export type CurrentPlanInfoBackend = { - billing: { - enabled: boolean - subscription: { - plan: BasicPlan - } - } - members: { - size: number - limit: number // total. 0 means unlimited - } - apps: { - size: number - limit: number // total. 0 means unlimited - } - annotation_quota_limit: { - size: number - limit: number // total. 0 means unlimited - } - documents_upload_quota: { - size: number - limit: number // total. 0 means unlimited - } - api_rate_limit?: BillingQuota - trigger_event?: BillingQuota - docs_processing: DocumentProcessingPriority - can_replace_logo: boolean - model_load_balancing_enabled: boolean - dataset_operator_enabled: boolean - education: { - enabled: boolean - activated: boolean - } - webapp_copyright_enabled: boolean - workspace_members: { - enabled?: boolean - size: number - limit: number - } - is_allow_transfer_workspace: boolean - knowledge_pipeline: { - publish_enabled: boolean - } - human_input_email_delivery_enabled: boolean -} - -export type SubscriptionUrlsBackend = { - url: string -} diff --git a/web/app/components/billing/usage-info/vector-space-info.tsx b/web/app/components/billing/usage-info/vector-space-info.tsx index 744e228dee8..1a039cb5640 100644 --- a/web/app/components/billing/usage-info/vector-space-info.tsx +++ b/web/app/components/billing/usage-info/vector-space-info.tsx @@ -1,13 +1,11 @@ 'use client' import type { FC } from 'react' -import type { BasicPlan } from '../type' import { RiHardDrive3Line } from '@remixicon/react' import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' import { consoleQuery } from '@/service/client' -import { Plan } from '../type' import UsageInfo from '../usage-info' import { getPlanVectorSpaceLimitMB } from '../utils' @@ -16,36 +14,15 @@ type Props = Readonly<{ }> // Storage threshold in MB - usage below this shows as "< 50 MB" -const STORAGE_THRESHOLD_MB = getPlanVectorSpaceLimitMB(Plan.sandbox) +const STORAGE_THRESHOLD_MB = getPlanVectorSpaceLimitMB('sandbox') const VectorSpaceInfo: FC = ({ className }) => { const { t } = useTranslation() const { plan } = useProviderContext() const { data: vectorSpace } = useQuery(consoleQuery.features.vectorSpace.get.queryOptions()) - const displayPlan = vectorSpace - ? { - ...plan, - usage: { - ...plan.usage, - vectorSpace: vectorSpace.size, - }, - total: { - ...plan.total, - vectorSpace: vectorSpace.limit, - }, - } - : plan - const { type, usage, total } = displayPlan - - // Determine total based on plan type (in MB), derived from ALL_PLANS config - const getTotalInMB = () => { - const planLimit = getPlanVectorSpaceLimitMB(type as BasicPlan) - // For known plans, use the config value; otherwise fall back to API response - return planLimit > 0 ? planLimit : total.vectorSpace - } - - const totalInMB = getTotalInMB() - const isSandbox = type === Plan.sandbox + const vectorSpaceUsage = vectorSpace?.size ?? plan.usage.vectorSpace + const vectorSpaceLimit = vectorSpace?.limit ?? getPlanVectorSpaceLimitMB(plan.type) + const isSandbox = plan.type === 'sandbox' return ( = ({ className }) => { Icon={RiHardDrive3Line} name={t(($) => $['usagePage.vectorSpace'], { ns: 'billing' })} tooltip={t(($) => $['usagePage.vectorSpaceTooltip'], { ns: 'billing' }) as string} - usage={usage.vectorSpace} - total={totalInMB} + usage={vectorSpaceUsage} + total={vectorSpaceLimit} unit="MB" unitPosition="inline" storageMode diff --git a/web/app/components/billing/utils/__tests__/index.spec.ts b/web/app/components/billing/utils/__tests__/index.spec.ts index 1a338f66e9a..0392d2a3da8 100644 --- a/web/app/components/billing/utils/__tests__/index.spec.ts +++ b/web/app/components/billing/utils/__tests__/index.spec.ts @@ -1,5 +1,4 @@ -import type { CurrentPlanInfoBackend } from '../../type' -import { DocumentProcessingPriority, Plan } from '../../type' +import type { GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' import { getPlanVectorSpaceLimitMB, parseCurrentPlan, parseVectorSpaceToMB } from '../index' describe('billing utils', () => { @@ -31,75 +30,85 @@ describe('billing utils', () => { // getPlanVectorSpaceLimitMB tests describe('getPlanVectorSpaceLimitMB', () => { it('should return correct vector space for sandbox plan', () => { - expect(getPlanVectorSpaceLimitMB(Plan.sandbox)).toBe(50) + expect(getPlanVectorSpaceLimitMB('sandbox')).toBe(50) }) it('should return correct vector space for professional plan', () => { - expect(getPlanVectorSpaceLimitMB(Plan.professional)).toBe(5 * 1024) + expect(getPlanVectorSpaceLimitMB('professional')).toBe(5 * 1024) }) it('should return correct vector space for team plan', () => { - expect(getPlanVectorSpaceLimitMB(Plan.team)).toBe(20 * 1024) - }) - - it('should return 0 for invalid plan', () => { - // @ts-expect-error - Testing invalid plan input - expect(getPlanVectorSpaceLimitMB('invalid')).toBe(0) + expect(getPlanVectorSpaceLimitMB('team')).toBe(20 * 1024) }) }) // parseCurrentPlan tests describe('parseCurrentPlan', () => { const createMockPlanData = ( - overrides: Partial = {}, - ): CurrentPlanInfoBackend => ({ - billing: { - enabled: true, - subscription: { - plan: Plan.sandbox, - }, + overrides: Partial = {}, + ): GetFeaturesResponse => ({ + annotation_quota_limit: { + size: 5, + limit: 10, }, - members: { - size: 1, - limit: 1, + api_rate_limit: { + usage: 0, + limit: 5000, + reset_date: -1, }, apps: { size: 2, limit: 5, }, - annotation_quota_limit: { - size: 5, - limit: 10, + billing: { + enabled: true, + subscription: { + interval: '', + plan: 'sandbox', + }, }, + can_replace_logo: false, + dataset_operator_enabled: false, + docs_processing: '', documents_upload_quota: { size: 20, limit: 0, }, - docs_processing: DocumentProcessingPriority.standard, - can_replace_logo: false, - model_load_balancing_enabled: false, - dataset_operator_enabled: false, education: { - enabled: false, activated: false, + enabled: false, }, - webapp_copyright_enabled: false, - workspace_members: { - size: 1, - limit: 1, - }, + human_input_email_delivery_enabled: false, is_allow_transfer_workspace: false, knowledge_pipeline: { publish_enabled: false, }, - human_input_email_delivery_enabled: false, + knowledge_rate_limit: 0, + members: { + size: 1, + limit: 1, + }, + model_load_balancing_enabled: false, + next_credit_reset_date: 0, + trigger_event: { + usage: 0, + limit: 3000, + reset_date: -1, + }, + vector_space: null, + webapp_copyright_enabled: false, + workspace_members: { + enabled: false, + size: 0, + limit: 0, + }, ...overrides, }) it('should parse plan type correctly', () => { const data = createMockPlanData() const result = parseCurrentPlan(data) - expect(result.type).toBe(Plan.sandbox) + expect(result.type).toBe('sandbox') }) it('should parse usage values correctly', () => { @@ -136,7 +145,8 @@ describe('billing utils', () => { billing: { enabled: true, subscription: { - plan: Plan.professional, + interval: '', + plan: 'professional', }, }, }) @@ -162,7 +172,7 @@ describe('billing utils', () => { api_rate_limit: { usage: 100, limit: 5000, - reset_date: null, + reset_date: 0, }, }) const result = parseCurrentPlan(data) @@ -176,7 +186,7 @@ describe('billing utils', () => { trigger_event: { usage: 50, limit: 3000, - reset_date: null, + reset_date: 0, }, }) const result = parseCurrentPlan(data) @@ -185,20 +195,12 @@ describe('billing utils', () => { expect(result.total.triggerEvents).toBe(3000) }) - it('should use fallback for api_rate_limit when not provided', () => { - const data = createMockPlanData() - const result = parseCurrentPlan(data) - - // Fallback to plan preset value for sandbox: 5000 - expect(result.total.apiRateLimit).toBe(5000) - }) - it('should convert 0 or -1 rate limits to NUM_INFINITE', () => { const data = createMockPlanData({ api_rate_limit: { usage: 0, limit: 0, - reset_date: null, + reset_date: 0, }, }) const result = parseCurrentPlan(data) @@ -208,7 +210,7 @@ describe('billing utils', () => { api_rate_limit: { usage: 0, limit: -1, - reset_date: null, + reset_date: 0, }, }) const result2 = parseCurrentPlan(data2) @@ -300,14 +302,6 @@ describe('billing utils', () => { expect(result.reset.apiRateLimit).toBeNull() }) - it('should handle missing apps field', () => { - const data = createMockPlanData() - // @ts-expect-error - Testing edge case - delete data.apps - const result = parseCurrentPlan(data) - expect(result.usage.buildApps).toBe(0) - }) - it('should return null for unrecognized date format', () => { const data = createMockPlanData({ api_rate_limit: { diff --git a/web/app/components/billing/utils/index.ts b/web/app/components/billing/utils/index.ts index ceb275c71a7..559c113fa9d 100644 --- a/web/app/components/billing/utils/index.ts +++ b/web/app/components/billing/utils/index.ts @@ -1,4 +1,4 @@ -import type { BasicPlan, BillingQuota, CurrentPlanInfoBackend } from '../type' +import type { CloudPlan, GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' import dayjs from 'dayjs' import { ALL_PLANS, NUM_INFINITE } from '@/app/components/billing/config' @@ -19,11 +19,8 @@ export const parseVectorSpaceToMB = (vectorSpace: string): number => { /** * Get the vector space limit in MB for a given plan type from ALL_PLANS config */ -export const getPlanVectorSpaceLimitMB = (planType: BasicPlan): number => { - const planInfo = ALL_PLANS[planType] - if (!planInfo) return 0 - - return parseVectorSpaceToMB(planInfo.vectorSpace) +export const getPlanVectorSpaceLimitMB = (planType: CloudPlan): number => { + return parseVectorSpaceToMB(ALL_PLANS[planType].vectorSpace) } const parseLimit = (limit: number) => { @@ -38,8 +35,8 @@ const parseRateLimit = (limit: number) => { return limit } -const normalizeResetDate = (resetDate?: number | null) => { - if (typeof resetDate !== 'number' || resetDate <= 0) return null +const normalizeResetDate = (resetDate: number) => { + if (resetDate <= 0) return null if (resetDate >= 1e12) return dayjs(resetDate) @@ -57,7 +54,7 @@ const normalizeResetDate = (resetDate?: number | null) => { return null } -const getResetInDaysFromDate = (resetDate?: number | null) => { +const getResetInDaysFromDate = (resetDate: number) => { const resetDay = normalizeResetDate(resetDate) if (!resetDay) return null @@ -67,46 +64,33 @@ const getResetInDaysFromDate = (resetDate?: number | null) => { return diff } -export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => { +export const parseCurrentPlan = (data: GetFeaturesResponse) => { const planType = data.billing.subscription.plan - const planPreset = ALL_PLANS[planType] const vectorSpaceLimit = getPlanVectorSpaceLimitMB(planType) - const resolveRateLimit = (limit?: number, fallback?: number) => { - const value = limit ?? fallback ?? 0 - return parseRateLimit(value) - } - const getQuotaUsage = (quota?: BillingQuota) => quota?.usage ?? 0 - const getQuotaResetInDays = (quota?: BillingQuota) => { - if (!quota) return null - return getResetInDaysFromDate(quota.reset_date) - } return { type: planType, usage: { vectorSpace: 0, - buildApps: data.apps?.size || 0, + buildApps: data.apps.size, teamMembers: data.members.size, annotatedResponse: data.annotation_quota_limit.size, documentsUploadQuota: data.documents_upload_quota.size, - apiRateLimit: getQuotaUsage(data.api_rate_limit), - triggerEvents: getQuotaUsage(data.trigger_event), + apiRateLimit: data.api_rate_limit.usage, + triggerEvents: data.trigger_event.usage, }, total: { vectorSpace: vectorSpaceLimit, - buildApps: parseLimit(data.apps?.limit) || 0, + buildApps: parseLimit(data.apps.limit), teamMembers: parseLimit(data.members.limit), annotatedResponse: parseLimit(data.annotation_quota_limit.limit), documentsUploadQuota: parseLimit(data.documents_upload_quota.limit), - apiRateLimit: resolveRateLimit( - data.api_rate_limit?.limit, - planPreset?.apiRateLimit ?? NUM_INFINITE, - ), - triggerEvents: resolveRateLimit(data.trigger_event?.limit, planPreset?.triggerEvents), + apiRateLimit: parseRateLimit(data.api_rate_limit.limit), + triggerEvents: parseRateLimit(data.trigger_event.limit), }, reset: { - apiRateLimit: getQuotaResetInDays(data.api_rate_limit), - triggerEvents: getQuotaResetInDays(data.trigger_event), + apiRateLimit: getResetInDaysFromDate(data.api_rate_limit.reset_date), + triggerEvents: getResetInDaysFromDate(data.trigger_event.reset_date), }, } } diff --git a/web/app/components/custom/custom-page/__tests__/index.spec.tsx b/web/app/components/custom/custom-page/__tests__/index.spec.tsx index 11e666e9652..43802516da6 100644 --- a/web/app/components/custom/custom-page/__tests__/index.spec.tsx +++ b/web/app/components/custom/custom-page/__tests__/index.spec.tsx @@ -1,10 +1,10 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ReactElement } from 'react' import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { consoleQuery } from '@/service/client' @@ -64,10 +64,10 @@ const mockUseModalContext = vi.mocked(useModalContext) const createProviderContext = ({ enableBilling = false, - planType = Plan.professional, + planType = 'professional', }: { enableBilling?: boolean - planType?: Plan + planType?: CloudPlan } = {}) => { return createMockProviderContextValue({ enableBilling, @@ -106,7 +106,7 @@ describe('CustomPage', () => { mockUseProviderContext.mockReturnValue( createProviderContext({ enableBilling: true, - planType: Plan.sandbox, + planType: 'sandbox', }), ) @@ -124,7 +124,7 @@ describe('CustomPage', () => { mockUseProviderContext.mockReturnValue( createProviderContext({ enableBilling: true, - planType: Plan.professional, + planType: 'professional', }), ) @@ -141,7 +141,7 @@ describe('CustomPage', () => { mockUseProviderContext.mockReturnValue( createProviderContext({ enableBilling: true, - planType: Plan.team, + planType: 'team', }), ) @@ -155,7 +155,7 @@ describe('CustomPage', () => { mockUseProviderContext.mockReturnValue( createProviderContext({ enableBilling: false, - planType: Plan.sandbox, + planType: 'sandbox', }), ) diff --git a/web/app/components/custom/custom-page/index.tsx b/web/app/components/custom/custom-page/index.tsx index 3c3756ebded..eaa0ae6a66b 100644 --- a/web/app/components/custom/custom-page/index.tsx +++ b/web/app/components/custom/custom-page/index.tsx @@ -1,7 +1,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { contactSalesUrl } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' @@ -15,9 +14,8 @@ const CustomPage = () => { }) const { plan, enableBilling } = useProviderContext() const { setShowPricingModal } = useModalContext() - const showBillingTip = - deploymentEdition === 'CLOUD' && enableBilling && plan.type === Plan.sandbox - const showContact = enableBilling && (plan.type === Plan.professional || plan.type === Plan.team) + const showBillingTip = deploymentEdition === 'CLOUD' && enableBilling && plan.type === 'sandbox' + const showContact = enableBilling && (plan.type === 'professional' || plan.type === 'team') return (
diff --git a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx index 2b4e7fa05de..9b5c2b87aa2 100644 --- a/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx +++ b/web/app/components/custom/custom-web-app-brand/hooks/__tests__/use-web-app-brand.spec.tsx @@ -1,3 +1,4 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { GetSystemFeaturesResponse } from '@dify/contracts/api/console/system-features/types.gen' import type { ChangeEvent } from 'react' import type { ConsoleStateFixture } from '@/test/console/state-fixture' @@ -6,7 +7,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockProviderContextValue } from '@/__mocks__/provider-context' import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils' import { defaultPlan } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { useProviderContext } from '@/context/provider-context' import { createConsoleQueryClient, renderHookWithConsoleQuery } from '@/test/console/query-data' import useWebAppBrand from '../use-web-app-brand' @@ -136,10 +136,10 @@ const testUserProfile = { const createProviderContext = ({ enableBilling = false, - planType = Plan.professional, + planType = 'professional', }: { enableBilling?: boolean - planType?: Plan + planType?: CloudPlan } = {}) => { return createMockProviderContextValue({ enableBilling, @@ -231,7 +231,7 @@ describe('useWebAppBrand', () => { mockUseProviderContext.mockReturnValue( createProviderContext({ enableBilling: true, - planType: Plan.sandbox, + planType: 'sandbox', }), ) customConfig = { ...customConfig, remove_webapp_brand: true } diff --git a/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts b/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts index 32bcce72f4b..c6851a67109 100644 --- a/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts +++ b/web/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand.ts @@ -6,7 +6,6 @@ import { useAtomValue } from 'jotai' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { getImageUploadErrorMessage, imageUpload } from '@/app/components/base/image-uploader/utils' -import { Plan } from '@/app/components/billing/type' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' @@ -29,7 +28,7 @@ const useWebAppBrand = () => { const updateCustomConfigMutation = useMutation( consoleQuery.workspaces.customConfig.post.mutationOptions(), ) - const isSandbox = enableBilling && plan.type === Plan.sandbox + const isSandbox = enableBilling && plan.type === 'sandbox' const uploading = uploadProgress > 0 && uploadProgress < 100 const webappLogo = customConfig?.replace_webapp_logo || '' const webappBrandRemoved = customConfig?.remove_webapp_brand ?? undefined diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index f3ebaec9958..e0456f650a9 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -6,7 +6,6 @@ import { RiArrowRightLine, RiLoader2Fill, RiTerminalBoxLine } from '@remixicon/r import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import { Plan } from '@/app/components/billing/type' import VectorSpaceAdmissionAlert from '@/app/components/datasets/common/vector-space-admission-alert' import { useProviderContext } from '@/context/provider-context' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' @@ -101,9 +100,9 @@ const EmbeddingProcess: FC = ({ router.push(`/datasets/${datasetId}/documents`) } - const showUpgradeBanner = enableBilling && plan.type !== Plan.team + const showUpgradeBanner = enableBilling && plan.type !== 'team' const showVectorSpaceUpgrade = - enableBilling && (plan.type === Plan.sandbox || plan.type === Plan.professional) + enableBilling && (plan.type === 'sandbox' || plan.type === 'professional') const vectorSpaceAdmissionError = statusList.find( (detail) => detail.error_code === 'vector_space_estimate_exceeded', ) diff --git a/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx b/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx index c13fb3cea81..07d59a7ce8f 100644 --- a/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create/step-one/__tests__/index.spec.tsx @@ -1,15 +1,29 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types' import type { NotionPage } from '@/models/common' import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/datasets' import { fireEvent, screen } from '@testing-library/react' -import { Plan } from '@/app/components/billing/type' import { DataSourceType } from '@/models/datasets' import { consoleQuery } from '@/service/client' import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data' import StepOne from '../index' -let mockPlan = { - type: Plan.professional, +let mockPlan: { + type: CloudPlan + usage: { + vectorSpace: number + buildApps: number + documentsUploadQuota: number + vectorStorageQuota: number + } + total: { + vectorSpace: number + buildApps: number + documentsUploadQuota: number + vectorStorageQuota: number + } +} = { + type: 'professional', usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 }, total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 }, } @@ -232,7 +246,7 @@ describe('StepOne', () => { vi.clearAllMocks() mockDatasetDetail = undefined mockPlan = { - type: Plan.professional, + type: 'professional', usage: { vectorSpace: 50, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 }, total: { vectorSpace: 100, buildApps: 0, documentsUploadQuota: 0, vectorStorageQuota: 0 }, } @@ -417,7 +431,7 @@ describe('StepOne', () => { it('should show plan upgrade modal when batch upload not supported and multiple files', () => { mockEnableBilling = true - mockPlan.type = Plan.sandbox + mockPlan.type = 'sandbox' const files = [createMockFileItem(), createMockFileItem()] render() @@ -428,7 +442,7 @@ describe('StepOne', () => { it('should show upgrade card immediately when in sandbox plan', () => { mockEnableBilling = true - mockPlan.type = Plan.sandbox + mockPlan.type = 'sandbox' render() @@ -462,7 +476,7 @@ describe('StepOne', () => { it('should require sandbox users to retry when vector space usage is unknown', () => { mockEnableBilling = true - mockPlan.type = Plan.sandbox + mockPlan.type = 'sandbox' mockPlan.usage.vectorSpace = 100 mockPlan.total.vectorSpace = 100 const files = [createMockFileItem()] @@ -477,7 +491,7 @@ describe('StepOne', () => { it('should allow paid users to continue when vector space usage is unknown', () => { mockEnableBilling = true - mockPlan.type = Plan.professional + mockPlan.type = 'professional' const files = [createMockFileItem()] render(, true) diff --git a/web/app/components/datasets/create/step-one/index.tsx b/web/app/components/datasets/create/step-one/index.tsx index 170eaf7437d..65867fdbf37 100644 --- a/web/app/components/datasets/create/step-one/index.tsx +++ b/web/app/components/datasets/create/step-one/index.tsx @@ -11,7 +11,6 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import NotionConnector from '@/app/components/base/notion-connector' import { NotionPageSelector } from '@/app/components/base/notion-page-selector' -import { Plan } from '@/app/components/billing/type' import VectorSpaceFull from '@/app/components/billing/vector-space-full' import VectorSpaceUnavailable from '@/app/components/billing/vector-space-unavailable' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' @@ -145,14 +144,14 @@ const StepOne = ({ ) const isCheckingVectorSpace = shouldCheckVectorSpace && !vectorSpace && isFetchingVectorSpacePlan const isVectorSpaceUnavailable = - shouldCheckVectorSpace && plan.type === Plan.sandbox && !!vectorSpace?.usage_unknown + shouldCheckVectorSpace && plan.type === 'sandbox' && !!vectorSpace?.usage_unknown const isVectorSpaceFull = !!vectorSpace && !vectorSpace.usage_unknown && vectorSpace.limit > 0 && vectorSpace.size >= vectorSpace.limit const isShowVectorSpaceFull = (allFileLoaded || hasNotion) && isVectorSpaceFull && enableBilling - const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox + const supportBatchUpload = !enableBilling || plan.type !== 'sandbox' const isNotionAuthed = useMemo( () => checkNotionAuth(authedDataSourceList), @@ -249,7 +248,7 @@ const StepOne = ({
)} - {enableBilling && plan.type === Plan.sandbox && ( + {enableBilling && plan.type === 'sandbox' && (
diff --git a/web/app/components/datasets/documents/create-from-pipeline/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/index.tsx index 42ac31c89a1..e8e959cfc88 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx @@ -11,7 +11,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' -import { Plan } from '@/app/components/billing/type' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { workspacePermissionKeysAtom, @@ -134,13 +133,13 @@ const CreateFormPipeline = () => { ) const isCheckingVectorSpace = shouldCheckVectorSpace && !vectorSpace && isFetchingVectorSpacePlan const isVectorSpaceUnavailable = - shouldCheckVectorSpace && plan.type === Plan.sandbox && !!vectorSpace?.usage_unknown + shouldCheckVectorSpace && plan.type === 'sandbox' && !!vectorSpace?.usage_unknown const isVectorSpaceFull = !!vectorSpace && !vectorSpace.usage_unknown && vectorSpace.limit > 0 && vectorSpace.size >= vectorSpace.limit - const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox + const supportBatchUpload = !enableBilling || plan.type !== 'sandbox' // UI state const { diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx index 55cecaf9f2b..4b546d6486c 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/__tests__/index.spec.tsx @@ -1,9 +1,9 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { Mock } from 'vitest' import type { DocumentIndexingStatus, IndexingStatusResponse } from '@/models/datasets' import type { InitialDocumentDetail } from '@/models/pipeline' import { fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { Plan } from '@/app/components/billing/type' import { IndexingType } from '@/app/components/datasets/create/step-two' import { DatasourceType } from '@/models/pipeline' import { renderWithConsoleQuery as render } from '@/test/console/query-data' @@ -37,7 +37,7 @@ vi.mock('@/next/link', () => ({ // Mock provider context let mockEnableBilling = false -let mockPlanType: Plan = Plan.sandbox +let mockPlanType: CloudPlan = 'sandbox' vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ enableBilling: mockEnableBilling, @@ -159,7 +159,7 @@ describe('EmbeddingProcess', () => { // Reset mock states mockEnableBilling = false - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' mockIndexingStatusData = [] // Setup default mock for fetchIndexingStatus @@ -211,7 +211,7 @@ describe('EmbeddingProcess', () => { it('should show upgrade banner when billing is enabled and plan is not team', () => { mockEnableBilling = true - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' const props = createDefaultProps() render() @@ -223,7 +223,7 @@ describe('EmbeddingProcess', () => { it('should not show upgrade banner when plan is team', () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' const props = createDefaultProps() render() @@ -235,7 +235,7 @@ describe('EmbeddingProcess', () => { it('should show upgrade banner for professional plan', () => { mockEnableBilling = true - mockPlanType = Plan.professional + mockPlanType = 'professional' const props = createDefaultProps() render() @@ -387,7 +387,7 @@ describe('EmbeddingProcess', () => { it('should not suggest an upgrade to team users', async () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' const doc1 = createMockDocument({ id: 'doc-1' }) mockIndexingStatusData = [ createMockIndexingStatus({ @@ -1074,7 +1074,7 @@ describe('EmbeddingProcess', () => { // Tests for priority label display it('should show priority label when billing is enabled', async () => { mockEnableBilling = true - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' const doc1 = createMockDocument({ id: 'doc-1' }) mockIndexingStatusData = [ createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }), diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx index 34e0eb2bdf9..0fb88ae74b1 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.tsx @@ -19,7 +19,6 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import NotionIcon from '@/app/components/base/notion-icon' import PriorityLabel from '@/app/components/billing/priority-label' -import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon' import VectorSpaceAdmissionAlert from '@/app/components/datasets/common/vector-space-admission-alert' @@ -120,8 +119,7 @@ const EmbeddingProcess = ({ ), [indexingStatusBatchDetail], ) - const showUpgrade = - enableBilling && (plan.type === Plan.sandbox || plan.type === Plan.professional) + const showUpgrade = enableBilling && (plan.type === 'sandbox' || plan.type === 'professional') const getSourceName = (id: string) => { const doc = documents.find((document) => document.id === id) @@ -173,7 +171,7 @@ const EmbeddingProcess = ({ planLimitMb={vectorSpaceAdmissionError.vector_space_limit_mb} /> )} - {enableBilling && plan.type !== Plan.team && ( + {enableBilling && plan.type !== 'team' && (
diff --git a/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx b/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx index 246891d84b3..e4130018b05 100644 --- a/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/detail/segment-add/__tests__/index.spec.tsx @@ -1,13 +1,13 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { SegmentImportStatus } from '@/types/dataset' import { fireEvent, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { Plan } from '@/app/components/billing/type' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { segmentImportStatus } from '@/types/dataset' import { SegmentAdd } from '../index' // Mock provider context -let mockPlan = { type: Plan.professional } +let mockPlan: { type: CloudPlan } = { type: 'professional' } let mockEnableBilling = true vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ @@ -19,7 +19,7 @@ vi.mock('@/context/provider-context', () => ({ describe('SegmentAdd', () => { beforeEach(() => { vi.clearAllMocks() - mockPlan = { type: Plan.professional } + mockPlan = { type: 'professional' } mockEnableBilling = true }) @@ -141,7 +141,7 @@ describe('SegmentAdd', () => { }) it('should show plan upgrade modal instead of batch modal for sandbox users', async () => { - mockPlan = { type: Plan.sandbox } + mockPlan = { type: 'sandbox' } const mockShowBatchModal = vi.fn() render() @@ -172,7 +172,7 @@ describe('SegmentAdd', () => { // Plan upgrade modal describe('Plan Upgrade Modal', () => { it('should show plan upgrade modal when sandbox user tries to add', () => { - mockPlan = { type: Plan.sandbox } + mockPlan = { type: 'sandbox' } render() fireEvent.click(screen.getByText(/list\.action\.addButton/i)) @@ -181,7 +181,7 @@ describe('SegmentAdd', () => { }) it('should not call showNewSegmentModal for sandbox users', () => { - mockPlan = { type: Plan.sandbox } + mockPlan = { type: 'sandbox' } const mockShowNewSegmentModal = vi.fn() render() @@ -191,7 +191,7 @@ describe('SegmentAdd', () => { }) it('should allow add when billing is disabled regardless of plan', () => { - mockPlan = { type: Plan.sandbox } + mockPlan = { type: 'sandbox' } mockEnableBilling = false const mockShowNewSegmentModal = vi.fn() render() @@ -202,7 +202,7 @@ describe('SegmentAdd', () => { }) it('should close plan upgrade modal when close button is clicked', () => { - mockPlan = { type: Plan.sandbox } + mockPlan = { type: 'sandbox' } render() // Show modal diff --git a/web/app/components/datasets/documents/detail/segment-add/index.tsx b/web/app/components/datasets/documents/detail/segment-add/index.tsx index 6eb85e42cb6..aff4c7582fe 100644 --- a/web/app/components/datasets/documents/detail/segment-add/index.tsx +++ b/web/app/components/datasets/documents/detail/segment-add/index.tsx @@ -10,7 +10,6 @@ import { import { useState } from 'react' import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' -import { Plan } from '@/app/components/billing/type' import { useProviderContext } from '@/context/provider-context' import { segmentImportStatus } from '@/types/dataset' @@ -32,7 +31,7 @@ export function SegmentAdd({ const { t } = useTranslation() const [isPlanUpgradeModalOpen, setIsPlanUpgradeModalOpen] = useState(false) const { plan, enableBilling } = useProviderContext() - const canAddChunks = !enableBilling || plan.type !== Plan.sandbox + const canAddChunks = !enableBilling || plan.type !== 'sandbox' const textColor = embedding ? 'text-components-button-secondary-accent-text-disabled' diff --git a/web/app/components/develop/__tests__/workflow-version-api-upgrade-notice.spec.tsx b/web/app/components/develop/__tests__/workflow-version-api-upgrade-notice.spec.tsx index 3aa8643c7bf..517deb775a3 100644 --- a/web/app/components/develop/__tests__/workflow-version-api-upgrade-notice.spec.tsx +++ b/web/app/components/develop/__tests__/workflow-version-api-upgrade-notice.spec.tsx @@ -1,11 +1,11 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { fireEvent, render, screen, within } from '@testing-library/react' -import { Plan } from '@/app/components/billing/type' import { WorkflowVersionApiContent, WorkflowVersionApiUpgradeNotice, } from '../workflow-version-api-upgrade-notice' -let mockPlanType = Plan.professional +let mockPlanType: CloudPlan = 'professional' let mockEnableBilling = true let mockIsFetchedPlan = true @@ -69,13 +69,13 @@ vi.mock('@/app/components/billing/upgrade-btn', () => ({ describe('WorkflowVersionApiUpgradeNotice', () => { beforeEach(() => { - mockPlanType = Plan.professional + mockPlanType = 'professional' mockEnableBilling = true mockIsFetchedPlan = true }) it('should not render before the plan is fetched', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' mockIsFetchedPlan = false render() @@ -86,7 +86,7 @@ describe('WorkflowVersionApiUpgradeNotice', () => { }) it('should not render when billing is disabled', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' mockEnableBilling = false render() @@ -105,7 +105,7 @@ describe('WorkflowVersionApiUpgradeNotice', () => { }) it('should show a small upgrade button and open the plan upgrade modal for sandbox plans', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' render() @@ -133,7 +133,7 @@ describe('WorkflowVersionApiUpgradeNotice', () => { describe('WorkflowVersionApiContent', () => { beforeEach(() => { - mockPlanType = Plan.professional + mockPlanType = 'professional' mockEnableBilling = true mockIsFetchedPlan = true }) @@ -147,7 +147,7 @@ describe('WorkflowVersionApiContent', () => { }) it('should hide content for sandbox plans', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' const { container } = render( <> @@ -168,7 +168,7 @@ describe('WorkflowVersionApiContent', () => { }) it('should show content when billing is disabled', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' mockEnableBilling = false mockIsFetchedPlan = false diff --git a/web/app/components/develop/workflow-version-api-upgrade-notice.tsx b/web/app/components/develop/workflow-version-api-upgrade-notice.tsx index ee18b30a2ca..9791a4aeb00 100644 --- a/web/app/components/develop/workflow-version-api-upgrade-notice.tsx +++ b/web/app/components/develop/workflow-version-api-upgrade-notice.tsx @@ -4,14 +4,13 @@ import type { PropsWithChildren } from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' -import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import { useProviderContext } from '@/context/provider-context' export const WorkflowVersionApiContent = ({ children }: PropsWithChildren) => { const { plan, enableBilling, isFetchedPlan } = useProviderContext() - if (enableBilling && (!isFetchedPlan || plan.type === Plan.sandbox)) return
+ if (enableBilling && (!isFetchedPlan || plan.type === 'sandbox')) return
return children } @@ -21,7 +20,7 @@ export const WorkflowVersionApiUpgradeNotice = () => { const { plan, enableBilling, isFetchedPlan } = useProviderContext() const [isPlanUpgradeModalOpen, setIsPlanUpgradeModalOpen] = useState(false) - if (!isFetchedPlan || !enableBilling || plan.type !== Plan.sandbox) return null + if (!isFetchedPlan || !enableBilling || plan.type !== 'sandbox') return null const title = t(($) => $['upgrade.workflowVersionRun.title']) const description = t(($) => $['upgrade.workflowVersionRun.description']) diff --git a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx index 666ebaf8cb8..6c94b14ae0f 100644 --- a/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx +++ b/web/app/components/explore/create-app-modal/__tests__/index.spec.tsx @@ -1,3 +1,4 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { CreateAppModalProps } from '../index' import type { UsagePlanInfo } from '@/app/components/billing/type' import { act, fireEvent, screen, waitFor, within } from '@testing-library/react' @@ -7,7 +8,6 @@ import { createMockPlanTotal, createMockPlanUsage, } from '@/__mocks__/provider-context' -import { Plan } from '@/app/components/billing/type' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { AppModeEnum } from '@/types/app' import CreateAppModal from '../index' @@ -57,7 +57,7 @@ const createPlanInfo = (buildApps: number): UsagePlanInfo => ({ }) let mockEnableBilling = false -let mockPlanType: Plan = Plan.team +let mockPlanType: CloudPlan = 'team' let mockUsagePlanInfo: UsagePlanInfo = createPlanInfo(1) let mockTotalPlanInfo: UsagePlanInfo = createPlanInfo(10) @@ -118,7 +118,7 @@ describe('CreateAppModal', () => { beforeEach(() => { vi.clearAllMocks() mockEnableBilling = false - mockPlanType = Plan.team + mockPlanType = 'team' mockUsagePlanInfo = createPlanInfo(1) mockTotalPlanInfo = createPlanInfo(10) hotkeyMocks.handlers.clear() @@ -223,7 +223,7 @@ describe('CreateAppModal', () => { describe('Quota Gating', () => { it('should show AppsFull and disable create when apps quota is reached', async () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' mockUsagePlanInfo = createPlanInfo(10) mockTotalPlanInfo = createPlanInfo(10) @@ -235,7 +235,7 @@ describe('CreateAppModal', () => { it('should allow saving when apps quota is reached in edit mode', async () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' mockUsagePlanInfo = createPlanInfo(10) mockTotalPlanInfo = createPlanInfo(10) @@ -281,7 +281,7 @@ describe('CreateAppModal', () => { it('should not submit when apps quota is reached in create mode', async () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' mockUsagePlanInfo = createPlanInfo(10) mockTotalPlanInfo = createPlanInfo(10) @@ -298,7 +298,7 @@ describe('CreateAppModal', () => { it('should submit when apps quota is reached in edit mode', async () => { mockEnableBilling = true - mockPlanType = Plan.team + mockPlanType = 'team' mockUsagePlanInfo = createPlanInfo(10) mockTotalPlanInfo = createPlanInfo(10) diff --git a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx index e653663f9cb..d884e22e7d7 100644 --- a/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/compliance.spec.tsx @@ -7,7 +7,6 @@ import { import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { Plan } from '@/app/components/billing/type' import { useModalContext } from '@/context/modal-context' import { baseProviderContextValue, useProviderContext } from '@/context/provider-context' import { getDocDownloadUrl } from '@/service/common' @@ -65,7 +64,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.sandbox, + type: 'sandbox', }, }) vi.mocked(useModalContext).mockReturnValue({ @@ -136,7 +135,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.team, + type: 'team', }, }) @@ -157,7 +156,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.team, + type: 'team', }, }) @@ -181,7 +180,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.team, + type: 'team', }, }) const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -216,7 +215,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.professional, + type: 'professional', }, }) @@ -244,7 +243,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.team, + type: 'team', }, }) @@ -284,7 +283,7 @@ describe('Compliance', () => { ...baseProviderContextValue, plan: { ...baseProviderContextValue.plan, - type: Plan.team, + type: 'team', }, }) @@ -319,24 +318,5 @@ describe('Compliance', () => { // getDocDownloadUrl should still have only been called once expect(getDocDownloadUrl).toHaveBeenCalledTimes(1) }, 20000) - - // canShowUpgradeTooltip=false: enterprise plan has empty tooltip text → no TooltipContent - it('should show upgrade badge with empty tooltip for enterprise plan', () => { - // Arrange - vi.mocked(useProviderContext).mockReturnValue({ - ...baseProviderContextValue, - plan: { - ...baseProviderContextValue.plan, - type: Plan.enterprise, - }, - }) - - // Act - openMenuAndRender() - - // Assert - enterprise is not in any download list, so upgrade badges should appear - // The key branch: upgradeTooltip[Plan.enterprise] = '' → canShowUpgradeTooltip=false - expect(screen.getAllByText('billing.upgradeBtn.encourageShort').length).toBeGreaterThan(0) - }) }) }) diff --git a/web/app/components/header/account-dropdown/compliance.tsx b/web/app/components/header/account-dropdown/compliance.tsx index 3865e84545a..fc7fdd3a730 100644 --- a/web/app/components/header/account-dropdown/compliance.tsx +++ b/web/app/components/header/account-dropdown/compliance.tsx @@ -1,3 +1,4 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ReactNode } from 'react' import { Button } from '@langgenius/dify-ui/button' import { @@ -13,7 +14,6 @@ import { useMutation } from '@tanstack/react-query' import { useQueryState } from 'nuqs' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { Plan } from '@/app/components/billing/type' import { settingsQueryParamName, settingsQueryParser, @@ -101,7 +101,7 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp const { plan } = useProviderContext() const { setShowPricingModal } = useModalContext() const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser) - const isFreePlan = plan.type === Plan.sandbox + const isFreePlan = plan.type === 'sandbox' const { isPending, mutate: downloadCompliance } = useMutation({ mutationKey: ['downloadCompliance', docName], @@ -117,11 +117,11 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp }, }) - const whichPlanCanDownloadCompliance = { - [DocName.SOC2_Type_I]: [Plan.professional, Plan.team], - [DocName.SOC2_Type_II]: [Plan.team], - [DocName.ISO_27001]: [Plan.team], - [DocName.GDPR]: [Plan.team, Plan.professional, Plan.sandbox], + const whichPlanCanDownloadCompliance: Record = { + [DocName.SOC2_Type_I]: ['professional', 'team'], + [DocName.SOC2_Type_II]: ['team'], + [DocName.ISO_27001]: ['team'], + [DocName.GDPR]: ['team', 'professional', 'sandbox'], } const isCurrentPlanCanDownload = whichPlanCanDownloadCompliance[docName].includes(plan.type) @@ -143,11 +143,10 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp setShowPricingModal, ]) - const upgradeTooltip: Record = { - [Plan.sandbox]: t(($) => $['compliance.sandboxUpgradeTooltip'], { ns: 'common' }), - [Plan.professional]: t(($) => $['compliance.professionalUpgradeTooltip'], { ns: 'common' }), - [Plan.team]: '', - [Plan.enterprise]: '', + const upgradeTooltip: Record = { + sandbox: t(($) => $['compliance.sandboxUpgradeTooltip'], { ns: 'common' }), + professional: t(($) => $['compliance.professionalUpgradeTooltip'], { ns: 'common' }), + team: '', } const labelTitle = typeof label === 'string' ? label : undefined diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx index ecd2b2d2759..f50fbb9f462 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -9,7 +9,6 @@ import { import { memo } from 'react' import { useTranslation } from 'react-i18next' import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' -import { Plan } from '@/app/components/billing/type' import { PlanBadge } from '@/app/components/header/plan-badge' type WorkplaceSelectorContentProps = { @@ -21,15 +20,9 @@ type WorkplaceSelectorItemProps = { workspace: TenantListItemResponse } -const workspacePlans = new Set(Object.values(Plan)) - -function isWorkspacePlan(plan: string | null | undefined): plan is Plan { - return !!plan && workspacePlans.has(plan) -} - const WorkplaceSelectorItem = memo(({ workspace }: WorkplaceSelectorItemProps) => { const workspaceName = workspace.name || workspace.id - const workspacePlan = isWorkspacePlan(workspace.plan) ? workspace.plan : Plan.sandbox + const workspacePlan = workspace.plan ?? 'sandbox' return ( diff --git a/web/app/components/header/account-setting/__tests__/index.spec.tsx b/web/app/components/header/account-setting/__tests__/index.spec.tsx index 6a647de57e3..c7f1d89c72b 100644 --- a/web/app/components/header/account-setting/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/index.spec.tsx @@ -145,7 +145,7 @@ const baseConsoleState: ConsoleStateFixture = { currentWorkspace: { id: '1', name: 'Workspace', - plan: '', + plan: null, role: 'owner', }, isCurrentWorkspaceManager: true, diff --git a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx index d7e6a64cff0..69fbea58f84 100644 --- a/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/__tests__/index.spec.tsx @@ -6,7 +6,6 @@ import { screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { vi } from 'vitest' import { createMockProviderContextValue } from '@/__mocks__/provider-context' -import { Plan } from '@/app/components/billing/type' import { useProviderContext } from '@/context/provider-context' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' import { useUpdateRolesOfMember } from '@/service/access-control/use-member-roles' @@ -399,7 +398,7 @@ describe('MembersPage', () => { createMockProviderContextValue({ enableBilling: true, plan: { - type: Plan.sandbox, + type: 'sandbox', total: { teamMembers: 5 } as unknown as ReturnType< typeof useProviderContext >['plan']['total'], @@ -420,7 +419,7 @@ describe('MembersPage', () => { createMockProviderContextValue({ enableBilling: true, plan: { - type: Plan.sandbox, + type: 'sandbox', total: { teamMembers: -1 } as unknown as ReturnType< typeof useProviderContext >['plan']['total'], @@ -438,7 +437,7 @@ describe('MembersPage', () => { createMockProviderContextValue({ enableBilling: true, plan: { - type: Plan.team, + type: 'team', total: { teamMembers: 50 } as unknown as ReturnType< typeof useProviderContext >['plan']['total'], @@ -448,8 +447,8 @@ describe('MembersPage', () => { renderMembersPage() - // Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout - // Plan.team is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout + // 'team' is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout + // 'team' is an unlimited member plan → isNotUnlimitedMemberPlan=false → non-billing layout expect(screen.getByText(/plansCommon\.memberAfter/i))!.toBeInTheDocument() }) @@ -548,7 +547,7 @@ describe('MembersPage', () => { createMockProviderContextValue({ enableBilling: true, plan: { - type: Plan.sandbox, + type: 'sandbox', total: { teamMembers: 5 } as unknown as ReturnType< typeof useProviderContext >['plan']['total'], @@ -719,7 +718,7 @@ describe('MembersPage', () => { createMockProviderContextValue({ enableBilling: true, plan: { - type: Plan.sandbox, + type: 'sandbox', total: { teamMembers: 2 } as unknown as ReturnType< typeof useProviderContext >['plan']['total'], diff --git a/web/app/components/header/account-setting/members-page/index.tsx b/web/app/components/header/account-setting/members-page/index.tsx index 9f10af7470d..10023f4ae62 100644 --- a/web/app/components/header/account-setting/members-page/index.tsx +++ b/web/app/components/header/account-setting/members-page/index.tsx @@ -10,7 +10,6 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' import { NUM_INFINITE } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import { useLocale } from '@/context/i18n' import { workspacePermissionKeysAtom } from '@/context/permission-state' @@ -50,8 +49,7 @@ const MembersPage = () => { >(null) const accounts = data?.accounts || [] const { plan, enableBilling, isAllowTransferWorkspace } = useProviderContext() - const isNotUnlimitedMemberPlan = - enableBilling && plan.type !== Plan.team && plan.type !== Plan.enterprise + const isNotUnlimitedMemberPlan = enableBilling && plan.type !== 'team' const isMemberFull = enableBilling && isNotUnlimitedMemberPlan && accounts.length >= plan.total.teamMembers const [editWorkspaceModalVisible, setEditWorkspaceModalVisible] = useState(false) diff --git a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx index 0b690bab975..0096ab1afb1 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/__tests__/index.spec.tsx @@ -1,21 +1,28 @@ +import type { GetFeaturesResponse } from '@dify/contracts/api/console/features/types.gen' import type { MemberInviteResponse } from '@dify/contracts/api/console/workspaces/types.gen' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import { vi } from 'vitest' -import { useProviderContextSelector } from '@/context/provider-context' import { useWorkspaceRoleList } from '@/service/access-control/use-workspace-roles' +import { seedFeatures } from '@/test/console/query-data' import { InviteModal } from '../index' -const { inviteMember } = vi.hoisted(() => ({ inviteMember: vi.fn() })) - -vi.mock('@/context/provider-context', () => ({ - useProviderContextSelector: vi.fn(), +const { fetchFeatures, inviteMember } = vi.hoisted(() => ({ + fetchFeatures: vi.fn(), + inviteMember: vi.fn(), })) + vi.mock('@/service/access-control/use-workspace-roles') vi.mock('@/service/client', () => ({ consoleQuery: { + features: { + get: { + queryKey: () => ['features'], + queryOptions: () => ({ queryKey: ['features'], queryFn: fetchFeatures }), + }, + }, workspaces: { current: { members: { @@ -33,7 +40,6 @@ vi.mock('@/service/client', () => ({ describe('InviteModal', () => { const onOpenChange = vi.fn() const onSend = vi.fn() - const refreshLicenseLimit = vi.fn() const createQueryClient = () => new QueryClient({ @@ -89,24 +95,23 @@ describe('InviteModal', () => { isFetchingNextPage: false, fetchNextPage: vi.fn(), } as unknown as ReturnType) - vi.mocked(useProviderContextSelector).mockImplementation((selector) => - selector({ - licenseLimit: { workspace_members: { size: 5, limit: 10 } }, - refreshLicenseLimit, - } as unknown as Parameters[0]), - ) }) const renderModal = ({ open = true, isEmailSetup = true, queryClient = createQueryClient(), + workspaceMembers = { enabled: true, size: 5, limit: 10 }, }: { open?: boolean isEmailSetup?: boolean queryClient?: QueryClient - } = {}) => - render( + workspaceMembers?: GetFeaturesResponse['workspace_members'] + } = {}) => { + const features = seedFeatures(queryClient, { workspace_members: workspaceMembers }) + fetchFeatures.mockResolvedValue(features) + + return render( { /> , ) + } const selectAdminRole = async (user: ReturnType) => { await user.click(screen.getByRole('combobox', { name: /members\.role/i })) @@ -181,7 +187,9 @@ describe('InviteModal', () => { invitation_results: [], tenant_id: 'tenant-id', } satisfies MemberInviteResponse) - renderModal() + const queryClient = createQueryClient() + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') + renderModal({ queryClient }) await addRecipients(user, 'First@Example.com, second@example.com; first@example.com') await selectAdminRole(user) @@ -196,7 +204,7 @@ describe('InviteModal', () => { }, }) }) - expect(refreshLicenseLimit).toHaveBeenCalled() + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ['features'] }) expect(onOpenChange).toHaveBeenCalledWith(false) expect(onSend).toHaveBeenCalledWith([]) }) @@ -466,18 +474,12 @@ describe('InviteModal', () => { it('warns but lets the backend decide whether recipients consume remaining seats', async () => { const user = userEvent.setup() - vi.mocked(useProviderContextSelector).mockImplementation((selector) => - selector({ - licenseLimit: { workspace_members: { size: 9, limit: 10 } }, - refreshLicenseLimit, - } as unknown as Parameters[0]), - ) inviteMember.mockResolvedValue({ result: 'success', invitation_results: [], tenant_id: 'tenant-id', } satisfies MemberInviteResponse) - renderModal() + renderModal({ workspaceMembers: { enabled: true, size: 9, limit: 10 } }) await addRecipients(user, 'one@example.com, two@example.com') await selectAdminRole(user) @@ -491,13 +493,7 @@ describe('InviteModal', () => { it('counts a manually typed recipient list before it is committed', async () => { const user = userEvent.setup() - vi.mocked(useProviderContextSelector).mockImplementation((selector) => - selector({ - licenseLimit: { workspace_members: { size: 9, limit: 10 } }, - refreshLicenseLimit, - } as unknown as Parameters[0]), - ) - renderModal() + renderModal({ workspaceMembers: { enabled: true, size: 9, limit: 10 } }) const input = screen.getByRole('textbox', { name: /members\.emailRecipients/i }) await user.type(input, 'one@example.com,two@example.com') @@ -662,6 +658,8 @@ describe('InviteModal', () => { it('resets the form after a controlled close', async () => { const user = userEvent.setup() const queryClient = createQueryClient() + const features = seedFeatures(queryClient) + fetchFeatures.mockResolvedValue(features) const ControlledInviteModal = () => { const [open, setOpen] = useState(false) diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 6d848f37547..1fafaa0b12f 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -13,11 +13,10 @@ import { DialogTrigger, } from '@langgenius/dify-ui/dialog' import { Form } from '@langgenius/dify-ui/form' -import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocale } from '@/context/i18n' -import { useProviderContextSelector } from '@/context/provider-context' import { consoleQuery } from '@/service/client' import { commonQueryKeys } from '@/service/use-common' import { mergeEmailRecipients } from './email-recipients' @@ -49,15 +48,18 @@ function InviteForm({ isEmailSetup, onOpenChange, onSend }: InviteFormProps) { const { t } = useTranslation() const locale = useLocale() const queryClient = useQueryClient() - const licenseLimit = useProviderContextSelector((state) => state.licenseLimit) - const refreshLicenseLimit = useProviderContextSelector((state) => state.refreshLicenseLimit) + const { data: features } = useQuery(consoleQuery.features.get.queryOptions()) const [recipients, setRecipients] = useState([]) const [draft, setDraft] = useState('') const [submissionError, setSubmissionError] = useState(null) const fieldErrors = submissionError?.kind === 'fields' ? submissionError.errors : undefined - const currentSize = licenseLimit.workspace_members.size ?? 0 - const memberLimit = licenseLimit.workspace_members.limit - const remainingSeats = memberLimit > 0 ? Math.max(memberLimit - currentSize, 0) : null + const memberLimit = features?.workspace_members.enabled + ? features.workspace_members + : features?.billing.enabled && features.members.limit > 0 + ? features.members + : undefined + const remainingSeats = + memberLimit && memberLimit.limit > 0 ? Math.max(memberLimit.limit - memberLimit.size, 0) : null const effectiveRecipients = mergeEmailRecipients(recipients, draft) const validRecipientCount = effectiveRecipients.filter(({ isValid }) => isValid).length const exceedsRemainingSeats = remainingSeats !== null && validRecipientCount > remainingSeats @@ -88,7 +90,7 @@ function InviteForm({ isEmailSetup, onOpenChange, onSend }: InviteFormProps) { }, { onSuccess: (response) => { - refreshLicenseLimit() + void queryClient.invalidateQueries({ queryKey: consoleQuery.features.get.queryKey() }) void queryClient.invalidateQueries({ queryKey: commonQueryKeys.members }) onOpenChange(false) onSend(response.invitation_results) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx index 5fb62233f84..c2f35ad934e 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/model-list-item.spec.tsx @@ -228,7 +228,7 @@ describe('ModelListItem', () => { expect(badge).toBeInTheDocument() }) - // Plan.sandbox: ConfigModel shown without load balancing enabled + // 'sandbox': ConfigModel shown without load balancing enabled it('should show ConfigModel for sandbox plan even without load balancing enabled', () => { // Arrange - set plan type to sandbox and keep load balancing disabled mockModelLoadBalancingEnabled = false diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index 5df9fb0b02a..8445e733fc2 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -10,7 +10,6 @@ import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' -import { Plan } from '@/app/components/billing/type' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext, useProviderContextSelector } from '@/context/provider-context' import { consoleQuery } from '@/service/client' @@ -132,7 +131,7 @@ const ModelListItem = ({ )} {canConfigureModels && - (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && + (modelLoadBalancingEnabled || plan.type === 'sandbox') && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status) && ( { beforeEach(() => { vi.clearAllMocks() - mockPlan(Plan.professional) + mockPlan('professional') mockUseModalContext.mockReturnValue({ setShowPricingModal, } as unknown as ReturnType) @@ -93,7 +93,7 @@ describe('WorkflowLogArchivesPage', () => { describe('Plan access', () => { it('should show upgrade guidance instead of archive content for sandbox workspaces', () => { // Arrange - mockPlan(Plan.sandbox) + mockPlan('sandbox') // Act renderPage() @@ -105,7 +105,7 @@ describe('WorkflowLogArchivesPage', () => { it('should open pricing modal from the sandbox upgrade guidance', () => { // Arrange - mockPlan(Plan.sandbox) + mockPlan('sandbox') renderPage() // Act @@ -117,7 +117,7 @@ describe('WorkflowLogArchivesPage', () => { it('should show archive content for paid workspaces', () => { // Arrange - mockPlan(Plan.professional) + mockPlan('professional') // Act renderPage() diff --git a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx index 2aa7c661759..a2fcc882ae5 100644 --- a/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx +++ b/web/app/components/header/account-setting/workflow-log-archives-page/index.tsx @@ -13,7 +13,6 @@ import { skipToken, useMutation, useQuery, useSuspenseQuery } from '@tanstack/re import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { SkeletonRectangle } from '@/app/components/base/skeleton' -import { Plan } from '@/app/components/billing/type' import { API_PREFIX } from '@/config' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -73,7 +72,7 @@ export default function WorkflowLogArchivesPage() { const [visibleArchiveMonthCount, setVisibleArchiveMonthCount] = useState(ARCHIVE_MONTH_PAGE_SIZE) const loadMoreRef = useRef(null) const canViewArchiveContent = - deploymentEdition === 'CLOUD' && enableBilling && plan.type !== Plan.sandbox + deploymentEdition === 'CLOUD' && enableBilling && plan.type !== 'sandbox' const archiveListQuery = useQuery( consoleQuery.workflowRunArchives.get.queryOptions({ enabled: canViewArchiveContent, diff --git a/web/app/components/header/plan-badge/__tests__/index.spec.tsx b/web/app/components/header/plan-badge/__tests__/index.spec.tsx index a707bf34e96..f414e9c5c53 100644 --- a/web/app/components/header/plan-badge/__tests__/index.spec.tsx +++ b/web/app/components/header/plan-badge/__tests__/index.spec.tsx @@ -1,81 +1,20 @@ -import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen' -import type { ReactElement } from 'react' -import type { Mock } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { vi } from 'vitest' -import { createMockProviderContextValue } from '@/__mocks__/provider-context' -import { useProviderContext } from '@/context/provider-context' -import { renderWithConsoleQuery } from '@/test/console/query-data' -import { Plan } from '../../../billing/type' +import { render, screen } from '@testing-library/react' import { PlanBadge } from '../index' -vi.mock('@/context/provider-context', () => ({ - useProviderContext: vi.fn(), - baseProviderContextValue: {}, -})) - describe('PlanBadge', () => { - const mockUseProviderContext = useProviderContext as Mock - let deploymentEdition: DeploymentEdition = 'CLOUD' - const render = (ui: ReactElement) => - renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: deploymentEdition } }) + it('should render sandbox plan', () => { + render() - beforeEach(() => { - vi.clearAllMocks() - deploymentEdition = 'CLOUD' - }) - - it('should return null if isFetchedPlan is false', () => { - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: false })) - const { container } = render() - expect(container.firstChild).toBeNull() - }) - - it('should render upgrade action as a button when onClick is provided', () => { - const handleClick = vi.fn() - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - - render() - - const button = screen.getByRole('button', { name: 'billing.upgradeBtn.encourageShort' }) - fireEvent.click(button) - expect(handleClick).toHaveBeenCalledTimes(1) - }) - - it('should render sandbox badge instead of upgrade badge in self-hosted edition', () => { - deploymentEdition = 'COMMUNITY' - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - - render() - - expect(screen.getByText(Plan.sandbox)).toBeInTheDocument() - expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() - expect(screen.queryByRole('button')).not.toBeInTheDocument() + expect(screen.getByText('sandbox')).toBeInTheDocument() }) it('should render professional badge when plan is professional', () => { - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - render() + render() expect(screen.getByText('pro')).toBeInTheDocument() }) it('should render team badge when plan is team', () => { - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - render() - expect(screen.getByText(Plan.team)).toBeInTheDocument() - }) - - it('should return null when plan is enterprise', () => { - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - const { container } = render() - expect(container.firstChild).toBeNull() - }) - - it('should trigger onClick when clicked', () => { - const handleClick = vi.fn() - mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ isFetchedPlan: true })) - render() - fireEvent.click(screen.getByRole('button', { name: Plan.team })) - expect(handleClick).toHaveBeenCalledTimes(1) + render() + expect(screen.getByText('team')).toBeInTheDocument() }) }) diff --git a/web/app/components/header/plan-badge/index.tsx b/web/app/components/header/plan-badge/index.tsx index 34190305e07..1561c23e6b3 100644 --- a/web/app/components/header/plan-badge/index.tsx +++ b/web/app/components/header/plan-badge/index.tsx @@ -1,105 +1,25 @@ -import type { ReactNode } from 'react' -import { RiGraduationCapFill } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' -import { useTranslation } from 'react-i18next' -import { useProviderContext } from '@/context/provider-context' -import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { SparklesSoft } from '../../base/icons/src/public/common' -import PremiumBadge, { PremiumBadgeButton } from '../../base/premium-badge' -import { Plan } from '../../billing/type' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' +import PremiumBadge from '../../base/premium-badge' -type PlanBadgeProps = { - plan: Plan - allowHover?: boolean - sandboxAsUpgrade?: boolean - onClick?: () => void -} - -function PlanBadgeShell({ - size, - color, - allowHover, - onClick, - children, -}: Pick & { - size?: 's' | 'm' - color: 'blue' | 'indigo' | 'gray' - children: ReactNode -}) { - if (onClick) { - return ( - - {children} - - ) +export function PlanBadge({ plan }: { plan: CloudPlan }) { + switch (plan) { + case 'sandbox': + return ( + + {plan} + + ) + case 'professional': + return ( + + pro + + ) + case 'team': + return ( + + {plan} + + ) } - - return ( - - {children} - - ) -} - -export function PlanBadge({ plan, allowHover, sandboxAsUpgrade = false, onClick }: PlanBadgeProps) { - const { data: deploymentEdition } = useSuspenseQuery({ - ...systemFeaturesQueryOptions(), - select: ({ deployment_edition }) => deployment_edition, - }) - const { isFetchedPlan, isEducationWorkspace } = useProviderContext() - const { t } = useTranslation() - - if (!isFetchedPlan) return null - if (plan === Plan.sandbox && sandboxAsUpgrade && deploymentEdition === 'CLOUD') { - return ( - - - ) - } - if (plan === Plan.sandbox) { - return ( - -
- {plan} -
-
- ) - } - if (plan === Plan.professional) { - return ( - -
- - {isEducationWorkspace && -
-
- ) - } - if (plan === Plan.team) { - return ( - -
- {plan} -
-
- ) - } - return null } diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index c6b7c74246a..57d5f2e5fe1 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -20,7 +20,6 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createStore, Provider as JotaiProvider } from 'jotai' import { queryClientAtom } from 'jotai-tanstack-query' -import { Plan } from '@/app/components/billing/type' import { DETAIL_SIDEBAR_STORAGE_KEY } from '@/app/components/detail-sidebar/storage' import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '@/app/components/explore/learn-dify/storage' import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' @@ -493,7 +492,7 @@ const consoleState: MainNavConsoleState = { currentWorkspace: { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.team, + plan: 'team', credits: 7500, role: 'owner', }, @@ -602,7 +601,7 @@ describe('MainNav', () => { { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.team, + plan: 'team', status: 'normal', created_at: 0, current: true, @@ -610,7 +609,7 @@ describe('MainNav', () => { { id: 'workspace-2', name: 'Evan Workspace', - plan: Plan.sandbox, + plan: 'sandbox', status: 'normal', created_at: 0, current: false, @@ -632,9 +631,8 @@ describe('MainNav', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, enableEducationPlan: false, - isEducationWorkspace: false, isFetchedPlan: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, } as ProviderContextState) ;(useModalContext as Mock).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, @@ -667,9 +665,9 @@ describe('MainNav', () => { it('renders primary navigation with the planned routes', () => { renderMainNav() - expect(screen.getAllByText(Plan.team)).toHaveLength(1) + expect(screen.getAllByText('team')).toHaveLength(1) expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent( - Plan.team, + 'team', ) expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/') expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps') @@ -767,9 +765,8 @@ describe('MainNav', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, enableEducationPlan: true, - isEducationWorkspace: false, isFetchedPlan: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, } as ProviderContextState) renderMainNav(defaultMainNavSystemFeatures, { @@ -780,7 +777,7 @@ describe('MainNav', () => { expect(await screen.findByText('EDU')).toBeInTheDocument() expect(screen.getByText('evan@example.com')).toBeInTheDocument() - expect(screen.getAllByText(Plan.team)).toHaveLength(1) + expect(screen.getAllByText('team')).toHaveLength(1) }) it('keeps unrestricted main routes visible for dataset operators while hiding roster', () => { @@ -1248,7 +1245,7 @@ describe('MainNav', () => { ...consoleState, currentWorkspace: { ...consoleState.currentWorkspace, - plan: Plan.sandbox, + plan: 'sandbox', }, } @@ -1263,7 +1260,7 @@ describe('MainNav', () => { ...consoleState, currentWorkspace: { ...consoleState.currentWorkspace, - plan: Plan.professional, + plan: 'professional', }, } diff --git a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx index a2bf6fbf589..2deecf75867 100644 --- a/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/support-menu.spec.tsx @@ -6,7 +6,6 @@ import { } from '@langgenius/dify-ui/dropdown-menu' import { fireEvent, screen } from '@testing-library/react' import { openZendeskWindow } from '@/app/components/base/zendesk/utils' -import { Plan } from '@/app/components/billing/type' import { mailToSupport } from '@/app/components/header/utils/util' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -74,7 +73,7 @@ describe('SupportMenu', () => { } ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, - plan: { type: Plan.team }, + plan: { type: 'team' }, }) ;(useModalContext as Mock).mockReturnValue({ setShowPricingModal: mockSetShowPricingModal, @@ -125,7 +124,7 @@ describe('SupportMenu', () => { it('renders contact us with upgrade badge for Cloud sandbox plan without dedicated support', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, }) renderSupportMenu() @@ -153,7 +152,7 @@ describe('SupportMenu', () => { it('hides upgrade contact for Cloud sandbox plan when billing is disabled', () => { ;(useProviderContext as Mock).mockReturnValue({ enableBilling: false, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, }) renderSupportMenu() @@ -168,7 +167,7 @@ describe('SupportMenu', () => { mockConfig.supportEmailAddress = 'support@example.com' ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, }) renderSupportMenu() @@ -186,7 +185,7 @@ describe('SupportMenu', () => { mockConfig.zendeskWidgetKey = '' ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, }) renderSupportMenu() @@ -196,7 +195,7 @@ describe('SupportMenu', () => { expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() expect(mailToSupport).toHaveBeenCalledWith( 'user@example.com', - Plan.sandbox, + 'sandbox', '1.0.0', 'support@example.com', ) @@ -206,7 +205,7 @@ describe('SupportMenu', () => { deploymentEdition = 'COMMUNITY' ;(useProviderContext as Mock).mockReturnValue({ enableBilling: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, }) renderSupportMenu() @@ -223,7 +222,7 @@ describe('SupportMenu', () => { expect(screen.queryByText('common.userProfile.contactUs')).not.toBeInTheDocument() expect(screen.getByText('common.userProfile.emailSupport')).toBeInTheDocument() - expect(mailToSupport).toHaveBeenCalledWith('user@example.com', Plan.team, '1.0.0', '') + expect(mailToSupport).toHaveBeenCalledWith('user@example.com', 'team', '1.0.0', '') expect( screen.getByRole('menuitem', { name: 'common.userProfile.emailSupport' }), ).toHaveAttribute('href', 'mailto:support@example.com') diff --git a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx index 132c289bd84..ecf67d387f2 100644 --- a/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx +++ b/web/app/components/main-nav/components/__tests__/workspace-card.spec.tsx @@ -7,7 +7,6 @@ import type { ProviderContextState } from '@/context/provider-context' import { zLicenseStatus } from '@dify/contracts/api/console/system-features/zod.gen' import { fireEvent, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { Plan } from '@/app/components/billing/type' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -99,7 +98,7 @@ vi.mock('@/service/client', async (importOriginal) => { const currentWorkspaceValue: GetWorkspacesCurrentSummaryResponse = { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.sandbox, + plan: 'sandbox', role: 'owner', credits: 7500, } @@ -157,7 +156,7 @@ describe('WorkspaceCard', () => { { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.sandbox, + plan: 'sandbox', status: 'normal', created_at: 0, current: true, @@ -165,7 +164,7 @@ describe('WorkspaceCard', () => { { id: 'workspace-2', name: 'Evan Workspace', - plan: Plan.team, + plan: 'team', status: 'normal', created_at: 0, current: false, @@ -177,9 +176,8 @@ describe('WorkspaceCard', () => { vi.mocked(useProviderContext).mockReturnValue({ enableBilling: true, enableEducationPlan: false, - isEducationWorkspace: false, isFetchedPlan: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, } as ProviderContextState) mockWorkspacePermissionKeys(['workspace.member.manage']) vi.mocked(useModalContext).mockReturnValue({ @@ -300,37 +298,36 @@ describe('WorkspaceCard', () => { it('uses the current workspace query for billing plan UI', () => { mockCurrentWorkspaceQuery({ ...currentWorkspaceValue, - plan: Plan.team, + plan: 'team', }) vi.mocked(useProviderContext).mockReturnValue({ enableBilling: false, enableEducationPlan: false, - isEducationWorkspace: false, isFetchedPlan: true, - plan: { type: Plan.sandbox }, + plan: { type: 'sandbox' }, } as ProviderContextState) renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } }) - expect(screen.getByText(Plan.team)).toBeInTheDocument() + expect(screen.getByText('team')).toBeInTheDocument() expect(screen.getByText('billing.upgradeBtn.plain')).toBeInTheDocument() - expect(screen.queryByText(Plan.sandbox)).not.toBeInTheDocument() + expect(screen.queryByText('sandbox')).not.toBeInTheDocument() expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() }) it('uses the original paid plan badge for paid workspaces', () => { mockCurrentWorkspaceQuery({ ...currentWorkspaceValue, - plan: Plan.team, + plan: 'team', }) renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } }) - expect(screen.getByText(Plan.team)).toBeInTheDocument() + expect(screen.getByText('team')).toBeInTheDocument() }) it('shows the Enterprise license status independently of the Cloud billing state', () => { mockCurrentWorkspaceQuery({ ...currentWorkspaceValue, - plan: '', + plan: null, }) renderWorkspaceCard({ systemFeatures: { @@ -342,7 +339,7 @@ describe('WorkspaceCard', () => { }) expect(screen.getByText('Enterprise')).toBeInTheDocument() - expect(screen.queryByText(Plan.sandbox)).not.toBeInTheDocument() + expect(screen.queryByText('sandbox')).not.toBeInTheDocument() }) it('opens workspace actions and switcher in a popover panel', async () => { @@ -406,7 +403,7 @@ describe('WorkspaceCard', () => { { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.sandbox, + plan: 'sandbox', status: 'normal', created_at: 1, last_opened_at: 20, @@ -415,7 +412,7 @@ describe('WorkspaceCard', () => { { id: 'workspace-2', name: 'Evan Workspace', - plan: Plan.team, + plan: 'team', status: 'normal', created_at: 3, last_opened_at: null, @@ -424,7 +421,7 @@ describe('WorkspaceCard', () => { { id: 'workspace-3', name: 'Atlas Workspace', - plan: Plan.team, + plan: 'team', status: 'normal', created_at: 2, last_opened_at: 30, diff --git a/web/app/components/main-nav/components/support-menu.tsx b/web/app/components/main-nav/components/support-menu.tsx index d4f8e7c87ec..49f92d8ffa0 100644 --- a/web/app/components/main-nav/components/support-menu.tsx +++ b/web/app/components/main-nav/components/support-menu.tsx @@ -2,7 +2,6 @@ import { DropdownMenuItem, DropdownMenuLinkItem } from '@langgenius/dify-ui/drop import { useSuspenseQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { openZendeskWindow } from '@/app/components/base/zendesk/utils' -import { Plan } from '@/app/components/billing/type' import { ExternalLinkIndicator, MenuItemContent, @@ -29,11 +28,11 @@ export default function SupportMenu() { }), }) const { setShowPricingModal } = useModalContext() - const hasDedicatedChannel = plan.type !== Plan.sandbox || Boolean(SUPPORT_EMAIL_ADDRESS.trim()) + const hasDedicatedChannel = plan.type !== 'sandbox' || Boolean(SUPPORT_EMAIL_ADDRESS.trim()) const shouldShowUpgradeContact = deploymentEdition === 'CLOUD' && enableBilling && - plan.type === Plan.sandbox && + plan.type === 'sandbox' && !hasDedicatedChannel const hasZendeskWidget = deploymentEdition === 'CLOUD' && Boolean(ZENDESK_WIDGET_KEY.trim()) diff --git a/web/app/components/main-nav/components/workspace-card.tsx b/web/app/components/main-nav/components/workspace-card.tsx index 9c43f0621d6..981e629537b 100644 --- a/web/app/components/main-nav/components/workspace-card.tsx +++ b/web/app/components/main-nav/components/workspace-card.tsx @@ -11,7 +11,6 @@ import { useQueryState } from 'nuqs' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar' -import { Plan } from '@/app/components/billing/type' import { settingsQueryParamName, settingsQueryParser, @@ -34,12 +33,6 @@ const workspaceMenuTriggerHeight = 36 const workspaceMenuAlignOffset = -28 const workspaceCardSkeletonClassName = 'animate-pulse rounded bg-text-quaternary opacity-20 motion-reduce:animate-none' -const workspacePlans = new Set(Object.values(Plan)) - -function isWorkspacePlan(plan: string | null | undefined): plan is Plan { - return !!plan && workspacePlans.has(plan) -} - function WorkspaceCardSkeleton({ showCloudBilling, showPlanAction, @@ -288,11 +281,11 @@ export function WorkspaceCard() { ) } - const workspacePlan = isWorkspacePlan(currentWorkspace.plan) ? currentWorkspace.plan : null - const hasBillingPlan = typeof currentWorkspace.plan === 'string' + const workspacePlan = currentWorkspace.plan + const hasBillingPlan = workspacePlan !== null const showCloudBilling = isCloudEdition && hasBillingPlan - const showPlanAction = showCloudBilling && workspacePlan !== null - const isFreePlan = workspacePlan === Plan.sandbox + const showPlanAction = showCloudBilling + const isFreePlan = workspacePlan === 'sandbox' const planActionLabel = t( ($) => $[isFreePlan ? 'upgradeBtn.encourageShort' : 'upgradeBtn.plain'], { ns: 'billing' }, diff --git a/web/app/components/main-nav/components/workspace-plan-badge.tsx b/web/app/components/main-nav/components/workspace-plan-badge.tsx index 1ee7aee7b66..b65995831e7 100644 --- a/web/app/components/main-nav/components/workspace-plan-badge.tsx +++ b/web/app/components/main-nav/components/workspace-plan-badge.tsx @@ -1,13 +1,13 @@ +import type { CloudPlan } from '@dify/contracts/api/console/workspaces/types.gen' import Badge from '@/app/components/base/badge' -import { Plan } from '@/app/components/billing/type' import { PlanBadge } from '@/app/components/header/plan-badge' type WorkspacePlanBadgeProps = { - plan: Plan + plan: CloudPlan } const WorkspacePlanBadge = ({ plan }: WorkspacePlanBadgeProps) => { - if (plan !== Plan.sandbox) return + if (plan !== 'sandbox') return return ( diff --git a/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx b/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx index 9fce1300b3d..591586b674f 100644 --- a/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx +++ b/web/app/components/step-by-step-tour/__tests__/mount.spec.tsx @@ -10,7 +10,6 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createStore, Provider as JotaiProvider } from 'jotai' import { queryClientAtom } from 'jotai-tanstack-query' -import { Plan } from '@/app/components/billing/type' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture' import { createSystemFeaturesFixture } from '@/test/console/system-features' @@ -401,7 +400,7 @@ function getMockAppContextState() { currentWorkspace: { id: 'workspace-1', name: 'Solar Studio', - plan: Plan.sandbox, + plan: 'sandbox', role: mockCurrentWorkspaceRole.value, }, isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager.value, diff --git a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx index 812e3712a4a..4f473499178 100644 --- a/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/__tests__/index.spec.tsx @@ -3,7 +3,6 @@ import type { ProviderContextState } from '@/context/provider-context' import { toast } from '@langgenius/dify-ui/toast' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import { Plan } from '@/app/components/billing/type' import { AuthHeaderPrefix, AuthType } from '@/app/components/tools/types' import { parseParamsSchema } from '@/service/tools' import EditCustomCollectionModal from '../index' @@ -65,7 +64,7 @@ describe('EditCustomCollectionModal', () => { }) mockUseProviderContext.mockReturnValue({ plan: { - type: Plan.sandbox, + type: 'sandbox', }, enableBilling: false, webappCopyrightEnabled: true, diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 1a99810e9b7..cafae7b2526 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -1,3 +1,4 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ReactElement } from 'react' import type { AppPublisherProps } from '@/app/components/app/app-publisher/types' import type { App } from '@/types/app' @@ -5,7 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' -import { Plan } from '@/app/components/billing/type' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' import { consoleQuery } from '@/service/client' import FeaturesTrigger from '../features-trigger' @@ -236,10 +236,10 @@ vi.mock('@/hooks/use-theme', () => ({ // Use real app store - global zustand mock will auto-reset between tests const createProviderContext = ({ - type = Plan.sandbox, + type = 'sandbox', isFetchedPlan = true, }: { - type?: Plan + type?: CloudPlan isFetchedPlan?: boolean }) => ({ plan: { type }, diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 4e10ef912e6..e9947de4bdb 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -12,7 +12,6 @@ import { useEdges } from 'reactflow' import { AppPublisher } from '@/app/components/app/app-publisher' import { useStore as useAppStore } from '@/app/components/app/store' import { useFeatures } from '@/app/components/base/features/hooks' -import { Plan } from '@/app/components/billing/type' // useWorkflowRunValidation, import { useHooksStore } from '@/app/components/workflow/hooks-store' import { @@ -118,7 +117,7 @@ const FeaturesTrigger = () => { if (nodeType === BlockEnum.Start || isTriggerNode(nodeType)) return count + 1 return count }, 0) - return isFetchedPlan && plan.type === Plan.sandbox && entryCount > 2 + return isFetchedPlan && plan.type === 'sandbox' && entryCount > 2 }, [nodes, plan.type, isFetchedPlan]) const hasHumanInputNode = useMemo(() => { diff --git a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx index 20eb4fbcfde..2f225ebc99b 100644 --- a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx @@ -1,6 +1,6 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { VersionHistory } from '@/types/workflow' import { fireEvent, screen } from '@testing-library/react' -import { Plan } from '@/app/components/billing/type' import { FlowType } from '@/types/common' import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { WorkflowVersion } from '../../types' @@ -11,7 +11,7 @@ const mockInvalidAllLastRun = vi.fn() const mockResetWorkflowVersionHistory = vi.fn() const mockHandleLoadBackupDraft = vi.fn() const mockHandleRefreshWorkflowDraft = vi.fn() -let mockPlanType = Plan.professional +let mockPlanType: CloudPlan = 'professional' let mockEnableBilling = true vi.mock('@/context/provider-context', () => ({ @@ -88,7 +88,7 @@ const createVersion = (overrides: Partial = {}): VersionHistory describe('HeaderInRestoring', () => { beforeEach(() => { vi.clearAllMocks() - mockPlanType = Plan.professional + mockPlanType = 'professional' mockEnableBilling = true }) @@ -142,7 +142,7 @@ describe('HeaderInRestoring', () => { }) it('should show plan upgrade modal instead of restoring when sandbox users click restore', () => { - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' renderWorkflowComponent(, { initialStoreState: { currentVersion: createVersion(), diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index 1c8584f8824..20a75844dc7 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -6,7 +6,6 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' -import { Plan } from '@/app/components/billing/type' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' @@ -49,7 +48,7 @@ const HeaderInRestoring = ({ onRestoreSettled }: HeaderInRestoringProps) => { const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() const canRestore = !!currentVersion?.id && !!configsMap?.flowId && currentVersion.version !== WorkflowVersion.Draft - const canUseWorkflowVersionAction = !enableBilling || plan.type !== Plan.sandbox + const canUseWorkflowVersionAction = !enableBilling || plan.type !== 'sandbox' const canEmitCollaborationEvents = configsMap?.flowType === FlowType.appFlow const handleCancelRestore = useCallback(() => { diff --git a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx index 2d358714fe7..607b8cdd7fa 100644 --- a/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/version-history-panel/__tests__/index.spec.tsx @@ -1,9 +1,9 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { Shape } from '../../../store' import type { VersionHistory } from '@/types/workflow' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useEffect, useRef } from 'react' -import { Plan } from '@/app/components/billing/type' import { renderWithConsoleQuery as render } from '@/test/console/query-data' import { VersionHistoryContextMenuOptions, WorkflowVersion } from '../../../types' @@ -25,7 +25,7 @@ const mockToast = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), })) -let mockPlanType = Plan.professional +let mockPlanType: CloudPlan = 'professional' let mockEnableBilling = true let mockPublishedEnvironments: VersionHistory['environments'] let mockHasNextPage = false @@ -284,7 +284,7 @@ describe('VersionHistoryPanel', () => { mockRestoreWorkflow.mockResolvedValue(undefined) mockUpdateWorkflow.mockResolvedValue(undefined) mockCurrentVersion = null - mockPlanType = Plan.professional + mockPlanType = 'professional' mockEnableBilling = true mockPublishedEnvironments = undefined mockHasNextPage = false @@ -387,7 +387,7 @@ describe('VersionHistoryPanel', () => { it('should show plan upgrade modal instead of restore confirmation for sandbox users', async () => { const { VersionHistoryPanel } = await import('../index') - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' render( { it('should show plan upgrade modal instead of exporting DSL for sandbox users', async () => { const { VersionHistoryPanel } = await import('../index') - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' render( { } }) -let mockPlanType = Plan.professional +let mockPlanType: CloudPlan = 'professional' let mockEnableBilling = true vi.mock('@/context/provider-context', () => ({ @@ -32,7 +32,7 @@ vi.mock('@/context/provider-context', () => ({ describe('ActionMenu', () => { beforeEach(() => { vi.clearAllMocks() - mockPlanType = Plan.professional + mockPlanType = 'professional' mockEnableBilling = true }) @@ -74,7 +74,7 @@ describe('ActionMenu', () => { it('shows upgrade buttons beside restore and export for sandbox users', async () => { const user = userEvent.setup() const handleClickActionMenuItem = vi.fn() - mockPlanType = Plan.sandbox + mockPlanType = 'sandbox' renderActionMenu( { const { t } = useTranslation() const pipelineId = useStore((s) => s.pipelineId) const { plan, enableBilling } = useProviderContext() - const shouldShowUpgrade = enableBilling && plan.type === Plan.sandbox + const shouldShowUpgrade = enableBilling && plan.type === 'sandbox' const deleteOperation = { key: VersionHistoryContextMenuOptions.delete, diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index 077244d0cd2..c8b4d7872f1 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next' import VersionInfoModal from '@/app/components/app/app-publisher/version-info-modal' import Divider from '@/app/components/base/divider' import { PlanUpgradeModal } from '@/app/components/billing/plan-upgrade-modal' -import { Plan } from '@/app/components/billing/type' import { getWorkflowVersionName } from '@/app/components/workflow/utils/version' import { useProviderContext } from '@/context/provider-context' import { userProfileQueryOptions } from '@/features/account-profile/client' @@ -67,7 +66,7 @@ export const VersionHistoryPanel = ({ const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) const [editModalOpen, setEditModalOpen] = useState(false) const { plan, enableBilling } = useProviderContext() - const canUseWorkflowVersionAction = !enableBilling || plan.type !== Plan.sandbox + const canUseWorkflowVersionAction = !enableBilling || plan.type !== 'sandbox' const workflowStore = useWorkflowStore() const { handleRestoreFromPublishedWorkflow, handleLoadBackupDraft } = useWorkflowRun() const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() diff --git a/web/app/education/apply/__tests__/page.spec.tsx b/web/app/education/apply/__tests__/page.spec.tsx index b690b129d7a..f231a09aa8e 100644 --- a/web/app/education/apply/__tests__/page.spec.tsx +++ b/web/app/education/apply/__tests__/page.spec.tsx @@ -2,13 +2,12 @@ import type { GetAccountProfileResponse } from '@dify/contracts/api/console/acco import { toast } from '@langgenius/dify-ui/toast' import { cleanup, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { Plan } from '@/app/components/billing/type' import EducationApplyPage from '@/app/education/apply/application-form' import { createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' let mockConsoleState: Record = {} -const mockFetchSubscriptionUrls = vi.hoisted(() => vi.fn()) +const mockGetSubscription = vi.hoisted(() => vi.fn()) const mockEducationAdd = vi.hoisted(() => vi.fn()) const mockSwitchWorkspace = vi.hoisted(() => vi.fn()) const mockWorkspaces = vi.hoisted(() => [ @@ -39,10 +38,6 @@ vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), })) -vi.mock('@/service/billing', () => ({ - fetchSubscriptionUrls: (...args: unknown[]) => mockFetchSubscriptionUrls(...args), -})) - vi.mock('@/service/use-common', () => ({ useLogout: () => ({ mutateAsync: vi.fn() }), })) @@ -57,6 +52,7 @@ vi.mock('@/service/client', () => ({ invoices: { get: vi.fn().mockResolvedValue({ url: 'https://billing.example.com' }), }, + subscription: { get: mockGetSubscription }, }, }, consoleQuery: { @@ -136,7 +132,7 @@ const renderPage = (isEducationAccount = true) => { educationStatus: { is_student: isEducationAccount }, workspacePermissionKeys: null, }) - return render(, { + return render(, { wrapper, }) } @@ -146,7 +142,7 @@ describe('EducationApplyPage billing boundary', () => { vi.clearAllMocks() cleanup() vi.spyOn(toast, 'error').mockImplementation(() => 'toast-id') - mockFetchSubscriptionUrls.mockResolvedValue({ url: window.location.href }) + mockGetSubscription.mockResolvedValue({ url: window.location.href }) mockSwitchWorkspace.mockResolvedValue(undefined) vi.stubGlobal('location', { href: 'https://console.example.com/education/apply?token=education-token', @@ -167,7 +163,9 @@ describe('EducationApplyPage billing boundary', () => { await user.click(screen.getByRole('button', { name: 'education.useEducationDiscount' })) await waitFor(() => { - expect(mockFetchSubscriptionUrls).toHaveBeenCalledWith(Plan.professional, 'year') + expect(mockGetSubscription).toHaveBeenCalledWith({ + query: { plan: 'professional', interval: 'year' }, + }) }) }) diff --git a/web/app/education/apply/application-form.tsx b/web/app/education/apply/application-form.tsx index 3a14aadd1a6..e13b62edc7e 100644 --- a/web/app/education/apply/application-form.tsx +++ b/web/app/education/apply/application-form.tsx @@ -16,7 +16,6 @@ import { useAtomValue } from 'jotai' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount' -import { Plan } from '@/app/components/billing/type' import { useDocLink } from '@/context/i18n' import { currentWorkspaceAtom, isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' @@ -66,7 +65,7 @@ const EducationApplyPage = ({ plan, token }: EducationApplyPageProps) => { const appliedEducationCase = (() => { if (!isCurrentWorkspaceManager) return AppliedEducationCase.noPaymentPermission - if (plan === Plan.sandbox) return AppliedEducationCase.eligible + if (plan === 'sandbox') return AppliedEducationCase.eligible return AppliedEducationCase.activeSubscription })() diff --git a/web/app/education/apply/applied-education-content.tsx b/web/app/education/apply/applied-education-content.tsx index fc32df65138..5b16c8f4462 100644 --- a/web/app/education/apply/applied-education-content.tsx +++ b/web/app/education/apply/applied-education-content.tsx @@ -8,7 +8,6 @@ import type { import type { ReactNode } from 'react' import { Select, SelectTrigger } from '@langgenius/dify-ui/select' import { useTranslation } from 'react-i18next' -import { Plan } from '@/app/components/billing/type' import { WorkplaceSelectorContent } from '@/app/components/header/account-dropdown/workplace-selector' import { PlanBadge } from '@/app/components/header/plan-badge' @@ -21,12 +20,6 @@ type AppliedEducationContentProps = { onSwitchWorkspace: (tenantId: string) => void } -const workspacePlans = new Set(Object.values(Plan)) - -function isWorkspacePlan(plan: string | null | undefined): plan is Plan { - return !!plan && workspacePlans.has(plan) -} - const AppliedEducationContent = ({ workspaces, currentWorkspace, @@ -37,11 +30,7 @@ const AppliedEducationContent = ({ }: AppliedEducationContentProps) => { const { t } = useTranslation() const currentWorkspaceInList = workspaces.find((workspace) => workspace.current) - const workspacePlan = isWorkspacePlan(currentWorkspaceInList?.plan) - ? currentWorkspaceInList.plan - : isWorkspacePlan(plan) - ? plan - : Plan.sandbox + const workspacePlan = currentWorkspaceInList?.plan ?? plan const workspaceName = currentWorkspaceInList?.name || currentWorkspace?.name const workspaceId = currentWorkspaceInList?.id || currentWorkspace?.id diff --git a/web/context/hooks/use-trigger-events-limit-modal.ts b/web/context/hooks/use-trigger-events-limit-modal.ts index 2cb12460036..8b017697983 100644 --- a/web/context/hooks/use-trigger-events-limit-modal.ts +++ b/web/context/hooks/use-trigger-events-limit-modal.ts @@ -1,8 +1,8 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { useSuspenseQuery } from '@tanstack/react-query' import dayjs from 'dayjs' import { useCallback, useEffect, useRef, useState } from 'react' import { NUM_INFINITE } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { isServer } from '@/utils/client' @@ -18,7 +18,7 @@ type TriggerEventsLimitModalState = TriggerEventsLimitModalContent & { } type TriggerPlanInfo = { - type: Plan + type: CloudPlan usage: { triggerEvents: number } total: { triggerEvents: number } reset: { triggerEvents?: number | null } @@ -63,19 +63,19 @@ export const useTriggerEventsLimitModal = ({ const isUnlimited = total.triggerEvents === NUM_INFINITE const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents - if (type === Plan.team || isUnlimited || !reachedLimit) { + if (type === 'team' || isUnlimited || !reachedLimit) { if (triggerEventsLimitModal) setTriggerEventsLimitModal(null) return } const triggerResetInDays = - type === Plan.professional && total.triggerEvents !== NUM_INFINITE + type === 'professional' && total.triggerEvents !== NUM_INFINITE ? (reset.triggerEvents ?? undefined) : undefined const cycleTag = (() => { if (typeof reset.triggerEvents === 'number') return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD') - if (type === Plan.sandbox) return dayjs().endOf('month').format('YYYY-MM-DD') + if (type === 'sandbox') return dayjs().endOf('month').format('YYYY-MM-DD') return 'none' })() const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}` diff --git a/web/context/modal-context.test.tsx b/web/context/modal-context.test.tsx index 5fedde87cac..596393fc391 100644 --- a/web/context/modal-context.test.tsx +++ b/web/context/modal-context.test.tsx @@ -1,8 +1,8 @@ +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { defaultPlan } from '@/app/components/billing/config' -import { Plan } from '@/app/components/billing/type' import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types' import { useModalContextSelector } from '@/context/modal-context' import { ModalContextProvider } from '@/context/modal-context-provider' @@ -46,8 +46,12 @@ type ResetShape = { apiRateLimit: number | null triggerEvents: number | null } -type PlanShape = Omit & { reset: ResetShape } -type PlanOverrides = Partial> & { +type PlanShape = Omit & { + type: CloudPlan + reset: ResetShape +} +type PlanOverrides = Partial> & { + type?: CloudPlan usage?: Partial total?: Partial reset?: Partial @@ -145,7 +149,7 @@ describe('ModalContextProvider trigger events limit modal', () => { it('opens the trigger events limit modal and persists dismissal in localStorage', async () => { const plan = createPlan({ - type: Plan.professional, + type: 'professional', usage: { triggerEvents: 3000 }, total: { triggerEvents: 3000 }, reset: { triggerEvents: 5 }, @@ -179,7 +183,7 @@ describe('ModalContextProvider trigger events limit modal', () => { it('relies on the in-memory guard when localStorage reads throw', async () => { const plan = createPlan({ - type: Plan.professional, + type: 'professional', usage: { triggerEvents: 200 }, total: { triggerEvents: 200 }, reset: { triggerEvents: 3 }, @@ -213,7 +217,7 @@ describe('ModalContextProvider trigger events limit modal', () => { it('falls back to the in-memory guard when localStorage.setItem fails', async () => { const plan = createPlan({ - type: Plan.professional, + type: 'professional', usage: { triggerEvents: 120 }, total: { triggerEvents: 120 }, reset: { triggerEvents: 2 }, @@ -245,7 +249,7 @@ describe('ModalContextProvider trigger events limit modal', () => { it('closes the trigger events limit modal and opens pricing when upgrading', async () => { const plan = createPlan({ - type: Plan.professional, + type: 'professional', usage: { triggerEvents: 400 }, total: { triggerEvents: 400 }, reset: { triggerEvents: 6 }, diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index 0d8db65969c..67b60e7f461 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -1,10 +1,9 @@ 'use client' import type { ReactNode } from 'react' -import type { ProviderContextState } from './provider-context' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import { defaultPlan } from '@/app/components/billing/config' import { parseCurrentPlan } from '@/app/components/billing/utils' @@ -14,7 +13,6 @@ import { } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ZENDESK_FIELD_IDS } from '@/config' import { deploymentEditionAtom } from '@/features/system-features/state' -import { fetchCurrentPlanInfo } from '@/service/billing' import { consoleQuery } from '@/service/client' import { commonQueryKeys, @@ -27,41 +25,10 @@ type ProviderContextProviderProps = { children: ReactNode } -type MemberInviteLimit = { - size: number - limit: number -} - -const unlimitedMemberInviteLimit: MemberInviteLimit = { - size: 0, - limit: 0, -} - -const resolveMemberInviteLimit = ( - data: Awaited>, -): MemberInviteLimit => { - if (!data) return unlimitedMemberInviteLimit - - if (data.workspace_members?.enabled) { - return { - size: data.workspace_members.size, - limit: data.workspace_members.limit, - } - } - - if (data.billing?.enabled && data.members?.limit > 0) { - return { - size: data.members.size, - limit: data.members.limit, - } - } - - return unlimitedMemberInviteLimit -} - export const ProviderContextProvider = ({ children }: ProviderContextProviderProps) => { const deploymentEdition = useAtomValue(deploymentEditionAtom) const queryClient = useQueryClient() + const featuresQuery = useQuery(consoleQuery.features.get.queryOptions()) const { data: providersData, isLoading: isLoadingModelProviders, @@ -70,29 +37,19 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro const { data: textGenerationModelList } = useModelListByType(ModelTypeEnum.textGeneration) const { data: supportRetrievalMethods } = useSupportRetrievalMethods() - const [plan, setPlan] = useState(defaultPlan) - const [isFetchedPlan, setIsFetchedPlan] = useState(false) - const [isFetchedPlanInfo, setIsFetchedPlanInfo] = useState(false) - const [enableBilling, setEnableBilling] = useState(true) - const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false) - const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) - const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false) - const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false) - const [licenseLimit, setLicenseLimit] = useState({ - workspace_members: { - size: 0, - limit: 0, - }, - }) - - const [enableEducationPlan, setEnableEducationPlan] = useState(false) - const [isEducationWorkspace, setIsEducationWorkspace] = useState(false) - const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false) - const [ - isAllowPublishAsCustomKnowledgePipelineTemplate, - setIsAllowPublishAsCustomKnowledgePipelineTemplate, - ] = useState(false) - const [humanInputEmailDeliveryEnabled, setHumanInputEmailDeliveryEnabled] = useState(false) + const features = featuresQuery.data + const enableBilling = features?.billing.enabled ?? false + const plan = enableBilling && features ? parseCurrentPlan(features) : defaultPlan + const isFetchedPlan = featuresQuery.isSuccess && enableBilling + const isFetchedPlanInfo = featuresQuery.isFetched + const enableEducationPlan = features?.education.enabled ?? false + const enableReplaceWebAppLogo = features?.can_replace_logo ?? false + const modelLoadBalancingEnabled = features?.model_load_balancing_enabled ?? false + const webappCopyrightEnabled = features?.webapp_copyright_enabled ?? false + const isAllowTransferWorkspace = features?.is_allow_transfer_workspace ?? false + const isAllowPublishAsCustomKnowledgePipelineTemplate = + features?.knowledge_pipeline.publish_enabled ?? false + const humanInputEmailDeliveryEnabled = features?.human_input_email_delivery_enabled ?? false const refreshModelProviders = () => Promise.all([ @@ -102,49 +59,10 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro queryClient.invalidateQueries({ queryKey: commonQueryKeys.modelProviderDetails }), ]).then(() => undefined) - const fetchPlan = async () => { - try { - const data = await fetchCurrentPlanInfo() - if (!data) { - console.error('Failed to fetch plan info: data is undefined') - return - } - - // set default value to avoid undefined error - setEnableBilling(data.billing?.enabled ?? false) - setEnableEducationPlan(data.education?.enabled ?? false) - setIsEducationWorkspace(data.education?.activated ?? false) - setEnableReplaceWebAppLogo(data.can_replace_logo ?? false) - - if (data.billing?.enabled) { - setPlan(parseCurrentPlan(data)) - setIsFetchedPlan(true) - } - - if (data.model_load_balancing_enabled) setModelLoadBalancingEnabled(true) - if (data.dataset_operator_enabled) setDatasetOperatorEnabled(true) - if (data.webapp_copyright_enabled) setWebappCopyrightEnabled(true) - setLicenseLimit({ workspace_members: resolveMemberInviteLimit(data) }) - if (data.is_allow_transfer_workspace) - setIsAllowTransferWorkspace(data.is_allow_transfer_workspace) - if (data.knowledge_pipeline?.publish_enabled) - setIsAllowPublishAsCustomKnowledgePipelineTemplate(data.knowledge_pipeline?.publish_enabled) - if (data.human_input_email_delivery_enabled) - setHumanInputEmailDeliveryEnabled(data.human_input_email_delivery_enabled) - } catch (error) { - console.error('Failed to fetch plan info:', error) - // set default value to avoid undefined error - setEnableBilling(false) - setEnableEducationPlan(false) - setIsEducationWorkspace(false) - setEnableReplaceWebAppLogo(false) - } finally { - setIsFetchedPlanInfo(true) - } - } - useEffect(() => { - fetchPlan() - }, []) + const refreshFeatures = () => + queryClient + .invalidateQueries({ queryKey: consoleQuery.features.get.key() }) + .then(() => undefined) // #region Zendesk conversation fields useEffect(() => { @@ -179,15 +97,11 @@ export const ProviderContextProvider = ({ children }: ProviderContextProviderPro isFetchedPlan, isFetchedPlanInfo, enableBilling, - onPlanInfoChanged: fetchPlan, + onPlanInfoChanged: refreshFeatures, enableReplaceWebAppLogo, modelLoadBalancingEnabled, - datasetOperatorEnabled, enableEducationPlan, - isEducationWorkspace, webappCopyrightEnabled, - licenseLimit, - refreshLicenseLimit: fetchPlan, isAllowTransferWorkspace, isAllowPublishAsCustomKnowledgePipelineTemplate, humanInputEmailDeliveryEnabled, diff --git a/web/context/provider-context.ts b/web/context/provider-context.ts index 73a69d20ae6..3ed84b46e93 100644 --- a/web/context/provider-context.ts +++ b/web/context/provider-context.ts @@ -1,10 +1,11 @@ 'use client' +import type { CloudPlan } from '@dify/contracts/api/console/features/types.gen' import type { ModelProviderPluginSummaryResponse, ModelProviderSummaryResponse, } from '@dify/contracts/api/console/workspaces/types.gen' -import type { Plan, UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type' +import type { UsagePlanInfo, UsageResetInfo } from '@/app/components/billing/type' import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { RETRIEVE_METHOD } from '@/types/app' import { noop } from 'es-toolkit/function' @@ -21,7 +22,7 @@ export type ProviderContextState = { supportRetrievalMethods: RETRIEVE_METHOD[] isAPIKeySet: boolean plan: { - type: Plan + type: CloudPlan usage: UsagePlanInfo total: UsagePlanInfo reset: UsageResetInfo @@ -32,17 +33,8 @@ export type ProviderContextState = { onPlanInfoChanged: () => void enableReplaceWebAppLogo: boolean modelLoadBalancingEnabled: boolean - datasetOperatorEnabled: boolean enableEducationPlan: boolean - isEducationWorkspace: boolean webappCopyrightEnabled: boolean - licenseLimit: { - workspace_members: { - size: number - limit: number - } - } - refreshLicenseLimit: () => void isAllowTransferWorkspace: boolean isAllowPublishAsCustomKnowledgePipelineTemplate: boolean humanInputEmailDeliveryEnabled: boolean @@ -64,17 +56,8 @@ export const baseProviderContextValue: ProviderContextState = { onPlanInfoChanged: noop, enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, - datasetOperatorEnabled: false, enableEducationPlan: false, - isEducationWorkspace: false, webappCopyrightEnabled: false, - licenseLimit: { - workspace_members: { - size: 0, - limit: 0, - }, - }, - refreshLicenseLimit: noop, isAllowTransferWorkspace: false, isAllowPublishAsCustomKnowledgePipelineTemplate: false, humanInputEmailDeliveryEnabled: false, diff --git a/web/service/billing.ts b/web/service/billing.ts deleted file mode 100644 index 075ab71adeb..00000000000 --- a/web/service/billing.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type' -import { get } from './base' - -export const fetchCurrentPlanInfo = () => { - return get('/features') -} - -export const fetchSubscriptionUrls = (plan: string, interval: string) => { - return get(`/billing/subscription?plan=${plan}&interval=${interval}`) -}