From fb10706c204179f7dd8807075012f95ccf6a0242 Mon Sep 17 00:00:00 2001 From: twwu Date: Fri, 15 Aug 2025 16:29:45 +0800 Subject: [PATCH] feat: refactor billing plans components to improve structure and add self-hosted plan item button; update pricing layout and translations --- web/app/components/billing/pricing/index.tsx | 2 +- .../billing/pricing/plan-switcher/index.tsx | 11 +- .../pricing/plans/cloud-plan-item/index.tsx | 4 +- .../plans/cloud-plan-item/list/item/index.tsx | 2 +- .../billing/pricing/plans/index.tsx | 8 +- .../plans/self-hosted-plan-item/button.tsx | 49 +++++ .../plans/self-hosted-plan-item/index.tsx | 167 ++++++------------ .../self-hosted-plan-item/list/index.tsx | 32 ++++ .../plans/self-hosted-plan-item/list/item.tsx | 21 +++ web/i18n/en-US/billing.ts | 10 +- web/i18n/zh-Hans/billing.ts | 6 +- 11 files changed, 179 insertions(+), 133 deletions(-) create mode 100644 web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx create mode 100644 web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx create mode 100644 web/app/components/billing/pricing/plans/self-hosted-plan-item/list/item.tsx diff --git a/web/app/components/billing/pricing/index.tsx b/web/app/components/billing/pricing/index.tsx index 112eeda788..9be04b9e93 100644 --- a/web/app/components/billing/pricing/index.tsx +++ b/web/app/components/billing/pricing/index.tsx @@ -39,7 +39,7 @@ const Pricing: FC = ({ className='fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] overflow-auto bg-saas-background' onClick={e => e.stopPropagation()} > -
+
= ({ onChangePlanRange, }) => { const { t } = useTranslation() + const isCloud = currentCategory === 'cloud' const tabs = { cloud: { @@ -52,10 +53,12 @@ const PlanSwitcher: FC = ({ onClick={onChangeCategory} />
- + {isCloud && ( + + )}
) 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 9e36cfd233..a4e3e819ae 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 @@ -102,8 +102,8 @@ const CloudPlanItem: FC = ({
{t(`${i18nPrefix}.description`)}
+ {/* Price */}
- {/* Price */} {isFreePlan && ( {t('billing.plansCommon.free')} )} @@ -111,7 +111,7 @@ const CloudPlanItem: FC = ({ <> {isYear && ${planInfo.price * 12}} ${isYear ? planInfo.price * 10 : planInfo.price} - + {t('billing.plansCommon.priceTip')} {t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)} diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/index.tsx index 8292a0297d..83d3d0d364 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/list/item/index.tsx @@ -12,7 +12,7 @@ const Item = ({ }: ItemProps) => { return (
- {label} + {label} {tooltip && ( + + } 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 new file mode 100644 index 0000000000..85904a8747 --- /dev/null +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { SelfHostedPlan } from '../../../type' +import { AwsMarketplace } from '@/app/components/base/icons/src/public/billing' +import { RiArrowRightLine } from '@remixicon/react' +import cn from '@/utils/classnames' +import { useTranslation } from 'react-i18next' + +const BUTTON_CLASSNAME = { + [SelfHostedPlan.community]: 'text-text-primary bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover', + [SelfHostedPlan.premium]: 'text-background-default bg-saas-background-inverted hover:bg-saas-background-inverted-hover', + [SelfHostedPlan.enterprise]: 'text-text-primary-on-surface bg-saas-dify-blue-static hover:bg-saas-dify-blue-static-hover', +} + +type ButtonProps = { + plan: SelfHostedPlan + handleGetPayUrl: () => void +} + +const Button = ({ + plan, + handleGetPayUrl, +}: ButtonProps) => { + const { t } = useTranslation() + const i18nPrefix = `billing.plans.${plan}` + const isPremiumPlan = plan === SelfHostedPlan.premium + + return ( + + ) +} + +export default React.memo(Button) 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 62b9e015e0..6044192adf 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,91 +1,55 @@ 'use client' -import type { FC, ReactNode } from 'react' -import React from 'react' +import type { FC } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { RiArrowRightUpLine, RiBrain2Line, RiCheckLine, RiQuestionLine } from '@remixicon/react' import { SelfHostedPlan } from '../../../type' import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../config' import Toast from '../../../../base/toast' -import Tooltip from '../../../../base/tooltip' -import { Asterisk, AwsMarketplace, Azure, Buildings, Diamond, GoogleCloud } from '../../../../base/icons/src/public/billing' -import type { PlanRange } from '../../plan-switcher/plan-range-switcher' import cn from '@/utils/classnames' import { useAppContext } from '@/context/app-context' - -type Props = { - plan: SelfHostedPlan - planRange: PlanRange - canPay: boolean -} - -const KeyValue = ({ label, tooltip, textColor, tooltipIconColor }: { icon: ReactNode; label: string; tooltip?: string; textColor: string; tooltipIconColor: string }) => { - return ( -
-
- -
-
{label}
- {tooltip && ( - -
- -
-
- )} -
- ) -} +import Button from './button' +import List from './list' +import { Azure, GoogleCloud } from '@/app/components/base/icons/src/public/billing' const style = { [SelfHostedPlan.community]: { - icon: , - title: 'text-text-primary', - price: 'text-text-primary', - priceTip: 'text-text-tertiary', - description: 'text-util-colors-gray-gray-600', - bg: 'border-effects-highlight-lightmode-off bg-background-section-burn', + icon:
, + bg: '', btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary', values: 'text-text-secondary', tooltipIconColor: 'text-text-tertiary', }, [SelfHostedPlan.premium]: { - icon: , - title: 'text-text-primary', - price: 'text-text-primary', - priceTip: 'text-text-tertiary', - description: 'text-text-warning', - bg: 'border-effects-highlight bg-background-section-burn', + icon:
, + bg: '', btnStyle: 'bg-third-party-aws hover:bg-third-party-aws-hover border border-components-button-primary-border text-text-primary-on-surface shadow-xs', values: 'text-text-secondary', tooltipIconColor: 'text-text-tertiary', }, [SelfHostedPlan.enterprise]: { - icon: , - title: 'text-text-primary-on-surface', - price: 'text-text-primary-on-surface', - priceTip: 'text-text-primary-on-surface', - description: 'text-text-primary-on-surface', - bg: 'border-effects-highlight bg-[#155AEF] text-text-primary-on-surface', + icon:
, + bg: '', btnStyle: 'bg-white/96 hover:opacity-85 border-[0.5px] border-components-button-secondary-border text-[#155AEF] shadow-xs', values: 'text-text-primary-on-surface', tooltipIconColor: 'text-text-primary-on-surface', }, } -const SelfHostedPlanItem: FC = ({ + +type SelfHostedPlanItemProps = { + plan: SelfHostedPlan +} + +const SelfHostedPlanItem: FC = ({ plan, }) => { const { t } = useTranslation() + const i18nPrefix = `billing.plans.${plan}` const isFreePlan = plan === SelfHostedPlan.community const isPremiumPlan = plan === SelfHostedPlan.premium - const i18nPrefix = `billing.plans.${plan}` const isEnterprisePlan = plan === SelfHostedPlan.enterprise const { isCurrentWorkspaceManager } = useAppContext() - const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[] - const handleGetPayUrl = () => { + + const handleGetPayUrl = useCallback(() => { // Only workspace manager can buy plan if (!isCurrentWorkspaceManager) { Toast.notify({ @@ -106,70 +70,51 @@ const SelfHostedPlanItem: FC = ({ if (isEnterprisePlan) window.location.href = contactSalesUrl - } - return ( -
-
-
-
- {isEnterprisePlan &&
} - {isEnterprisePlan &&
} -
-
-
- {style[plan].icon} -
-
{t(`${i18nPrefix}.name`)}
-
-
{t(`${i18nPrefix}.description`)}
-
-
-
-
{t(`${i18nPrefix}.price`)}
- {!isFreePlan - && - {t(`${i18nPrefix}.priceTip`)} - } -
-
+ }, [isCurrentWorkspaceManager, isFreePlan, isPremiumPlan, isEnterprisePlan, t]) -
- {t(`${i18nPrefix}.btnText`)} - {isPremiumPlan - && <> -
- -
- - } + return ( +
+
+
+ {style[plan].icon} +
+
{t(`${i18nPrefix}.name`)}
+
{t(`${i18nPrefix}.description`)}
+
-
{t(`${i18nPrefix}.includesTitle`)}
-
- {features.map(v => - } - label={v} - />)} + {/* Price */} +
+
{t(`${i18nPrefix}.price`)}
+ {!isFreePlan && ( + + {t(`${i18nPrefix}.priceTip`)} + + )}
- {isPremiumPlan &&
+
+ + {isPremiumPlan && ( +
-
+
-
+
- {t('billing.plans.premium.comingSoon')} -
} -
+ + {t('billing.plans.premium.comingSoon')} + +
+ )}
) } 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 new file mode 100644 index 0000000000..3752fbeed6 --- /dev/null +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import type { SelfHostedPlan } from '@/app/components/billing/type' +import { useTranslation } from 'react-i18next' +import Item from './item' + +type ListProps = { + plan: SelfHostedPlan +} + +const List = ({ + plan, +}: ListProps) => { + const { t } = useTranslation() + const i18nPrefix = `billing.plans.${plan}` + const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[] + + return ( +
+
+ {t(`${i18nPrefix}.includesTitle`)} +
+ {features.map(feature => + , + )} +
+ ) +} + +export default React.memo(List) diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/item.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/item.tsx new file mode 100644 index 0000000000..ce708194a9 --- /dev/null +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/item.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { RiCheckLine } from '@remixicon/react' + +type ItemProps = { + label: string +} + +const Item = ({ + label, +}: ItemProps) => { + return ( +
+
+ +
+ {label} +
+ ) +} + +export default React.memo(Item) diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts index ddc690712d..3a4822f650 100644 --- a/web/i18n/en-US/billing.ts +++ b/web/i18n/en-US/billing.ts @@ -126,9 +126,9 @@ const translation = { community: { name: 'Community', for: 'For Individual Users, Small Teams, or Non-commercial Projects', - description: 'For Individual Users, Small Teams, or Non-commercial Projects', + description: 'For open-source enthusiasts, individual developers, and non-commercial projects', price: 'Free', - btnText: 'Get Started with Community', + btnText: 'Get Started', includesTitle: 'Free Features:', features: [ 'All Core Features Released Under the Public Repository', @@ -139,10 +139,10 @@ const translation = { premium: { name: 'Premium', for: 'For Mid-sized Organizations and Teams', - description: 'For Mid-sized Organizations and Teams', + description: 'For Mid-sized organizations needing deployment flexibility and enhanced support', price: 'Scalable', priceTip: 'Based on Cloud Marketplace', - btnText: 'Get Premium in', + btnText: 'Get Premium on', includesTitle: 'Everything from Community, plus:', comingSoon: 'Microsoft Azure & Google Cloud Support Coming Soon', features: [ @@ -155,7 +155,7 @@ const translation = { enterprise: { name: 'Enterprise', for: 'For large-sized Teams', - description: 'For Enterprise Require Organization-wide Security, Compliance, Scalability, Control and More Advanced Features', + description: 'For enterprise requiring organization-grade security, compliance, scalability, control and custom solutions', price: 'Custom', priceTip: 'Annual Billing Only', btnText: 'Contact Sales', diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts index 6b4150a211..aad35dac15 100644 --- a/web/i18n/zh-Hans/billing.ts +++ b/web/i18n/zh-Hans/billing.ts @@ -125,7 +125,7 @@ const translation = { community: { name: 'Community', for: '适用于个人用户、小型团队或非商业项目', - description: '适用于个人用户、小型团队或非商业项目', + description: '适用于开源爱好者、个人开发者以及非商业项目', price: '免费', btnText: '开始使用', includesTitle: '免费功能:', @@ -138,7 +138,7 @@ const translation = { premium: { name: 'Premium', for: '对于中型组织和团队', - description: '对于中型组织和团队', + description: '适合需要部署灵活性和增强支持的中型组织和团队', price: '可扩展', priceTip: '基于云市场', btnText: '获得 Premium 版', @@ -154,7 +154,7 @@ const translation = { enterprise: { name: 'Enterprise', for: '适合大人员规模的团队', - description: '对于需要组织范围内的安全性、合规性、可扩展性、控制和更高级功能的企业', + description: '适合需要组织级安全性、合规性、可扩展性、控制和定制解决方案的企业', price: '定制', priceTip: '仅按年计费', btnText: '联系销售',