From 513d56afecc18304c2a345365b5f5fa47d8768d4 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Mon, 17 Jul 2023 19:16:10 +0800 Subject: [PATCH] feat(console): add downgrade plan confirm modal (#4161) --- .../src/assets/icons/descend-arrow.svg | 3 + .../src/ds-components/ConfirmModal/index.tsx | 4 + .../src/ds-components/ModalLayout/index.tsx | 2 +- .../QuotaDiffItem/index.module.scss | 29 ++++++ .../PlanQuotaDiffList/QuotaDiffItem/index.tsx | 53 +++++++++++ .../PlanQuotaDiffList/index.module.scss | 24 +++++ .../PlanQuotaDiffList/index.tsx | 52 +++++++++++ .../index.module.scss | 17 ++++ .../DowngradeConfirmModalContent/index.tsx | 52 +++++++++++ .../index.module.scss | 26 ++++++ .../index.tsx | 83 +++++++++++++++++ .../SwitchPlanActionBar/index.tsx | 42 +++++++++ .../Subscription/quota-item-phrases.ts | 91 +++++++++++++++++++ packages/console/src/types/subscriptions.ts | 2 +- 14 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 packages/console/src/assets/icons/descend-arrow.svg create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.module.scss create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.tsx create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.module.scss create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.tsx create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.module.scss create mode 100644 packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.tsx create mode 100644 packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.module.scss create mode 100644 packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.tsx create mode 100644 packages/console/src/pages/TenantSettings/Subscription/quota-item-phrases.ts diff --git a/packages/console/src/assets/icons/descend-arrow.svg b/packages/console/src/assets/icons/descend-arrow.svg new file mode 100644 index 000000000..9e355f630 --- /dev/null +++ b/packages/console/src/assets/icons/descend-arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/console/src/ds-components/ConfirmModal/index.tsx b/packages/console/src/ds-components/ConfirmModal/index.tsx index 3bc98e7d4..ac7ac9172 100644 --- a/packages/console/src/ds-components/ConfirmModal/index.tsx +++ b/packages/console/src/ds-components/ConfirmModal/index.tsx @@ -8,6 +8,7 @@ import Button from '@/ds-components/Button'; import * as modalStyles from '@/scss/modal.module.scss'; import ModalLayout from '../ModalLayout'; +import type { Props as ModalLayoutProps } from '../ModalLayout'; import * as styles from './index.module.scss'; @@ -21,6 +22,7 @@ export type ConfirmModalProps = { isOpen: boolean; isConfirmButtonDisabled?: boolean; isLoading?: boolean; + size?: ModalLayoutProps['size']; onCancel?: () => void; onConfirm?: () => void; }; @@ -35,6 +37,7 @@ function ConfirmModal({ isOpen, isConfirmButtonDisabled = false, isLoading = false, + size, onCancel, onConfirm, }: ConfirmModalProps) { @@ -63,6 +66,7 @@ function ConfirmModal({ } className={classNames(styles.content, className)} + size={size} onClose={onCancel} > {children} diff --git a/packages/console/src/ds-components/ModalLayout/index.tsx b/packages/console/src/ds-components/ModalLayout/index.tsx index 06bfcfced..fedc4513b 100644 --- a/packages/console/src/ds-components/ModalLayout/index.tsx +++ b/packages/console/src/ds-components/ModalLayout/index.tsx @@ -10,7 +10,7 @@ import IconButton from '../IconButton'; import * as styles from './index.module.scss'; -type Props = { +export type Props = { children: ReactNode; footer?: ReactNode; onClose?: () => void; diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.module.scss b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.module.scss new file mode 100644 index 000000000..4d542cfe4 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.module.scss @@ -0,0 +1,29 @@ +@use '@/scss/underscore' as _; + +.item { + margin-left: _.unit(4); + + &.withChangeState { + list-style-type: none; + margin-left: unset; + } + + .itemContent { + display: flex; + align-items: center; + gap: _.unit(2); + + .icon { + width: 16px; + height: 16px; + } + + &.notCapable { + text-decoration: line-through; + + .icon { + color: var(--color-error-hover); + } + } + } +} diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.tsx new file mode 100644 index 000000000..63b76f4f1 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/QuotaDiffItem/index.tsx @@ -0,0 +1,53 @@ +import { conditional } from '@silverhand/essentials'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; + +import DescendArrow from '@/assets/icons/descend-arrow.svg'; +import Failed from '@/assets/icons/failed.svg'; +import { + quotaItemUnlimitedPhrasesMap, + quotaItemPhrasesMap, + quotaItemLimitedPhrasesMap, +} from '@/pages/TenantSettings/Subscription/quota-item-phrases'; +import { type SubscriptionPlanQuota } from '@/types/subscriptions'; + +import * as styles from './index.module.scss'; + +type Props = { + hasIcon?: boolean; + quotaKey: keyof SubscriptionPlanQuota; + quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota]; +}; + +function QuotaDiffItem({ hasIcon = false, quotaKey, quotaValue }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription.quota_item' }); + const isUnlimited = quotaValue === null; + const isNotCapable = quotaValue === 0 || quotaValue === false; + const isLimited = Boolean(quotaValue); + + const Icon = isNotCapable ? Failed : DescendArrow; + + return ( +
  • + + {hasIcon && } + + {isUnlimited && <>{t(quotaItemUnlimitedPhrasesMap[quotaKey])}} + {isNotCapable && <>{t(quotaItemPhrasesMap[quotaKey])}} + {isLimited && ( + <> + {t( + quotaItemLimitedPhrasesMap[quotaKey], + conditional(typeof quotaValue === 'number' && { count: quotaValue }) ?? {} + )} + + )} + + +
  • + ); +} + +export default QuotaDiffItem; diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.module.scss b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.module.scss new file mode 100644 index 000000000..21b784b12 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.module.scss @@ -0,0 +1,24 @@ +@use '@/scss/underscore' as _; + +.container { + flex: 1; + background-color: var(--color-layer-2); + border-radius: 8px; + padding: _.unit(5); + + .title { + font: var(--font-title-2); + margin-bottom: _.unit(3); + } + + .list { + font: var(--font-body-2); + padding-inline-start: 0; + + > li { + &:not(:first-child) { + margin-top: _.unit(3); + } + } + } +} diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.tsx new file mode 100644 index 000000000..6b75cd060 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/PlanQuotaDiffList/index.tsx @@ -0,0 +1,52 @@ +import { useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import PlanName from '@/components/PlanName'; +import { type SubscriptionPlanQuota } from '@/types/subscriptions'; + +import QuotaDiffItem from './QuotaDiffItem'; +import * as styles from './index.module.scss'; + +type Props = { + planName: string; + quotaDiff: Partial; + isTarget?: boolean; +}; + +function PlanQuotaDiffList({ planName, quotaDiff, isTarget = false }: Props) { + const { t } = useTranslation(undefined, { + keyPrefix: 'admin_console.subscription.downgrade_modal', + }); + + // Todo: @xiaoyijun LOG-6540 order keys + // eslint-disable-next-line no-restricted-syntax + const entries = useMemo(() => Object.entries(quotaDiff), [quotaDiff]) as Array< + [keyof SubscriptionPlanQuota, SubscriptionPlanQuota[keyof SubscriptionPlanQuota]] + >; + + return ( +
    +
    + , + }} + > + {t(isTarget ? 'after' : 'before')} + +
    +
      + {entries.map(([quotaKey, quotaValue]) => ( + + ))} +
    +
    + ); +} + +export default PlanQuotaDiffList; diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.module.scss b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.module.scss new file mode 100644 index 000000000..f29fef81f --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.module.scss @@ -0,0 +1,17 @@ +@use '@/scss/underscore' as _; + +.container { + > :not(:first-child) { + margin: _.unit(6) 0 0; + } +} + +.description { + font: var(--font-body-2); +} + +.content { + display: flex; + justify-content: space-between; + gap: _.unit(3); +} diff --git a/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.tsx new file mode 100644 index 000000000..2ff99a350 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/DowngradeConfirmModalContent/index.tsx @@ -0,0 +1,52 @@ +import { diff } from 'deep-object-diff'; +import { useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import PlanName from '@/components/PlanName'; +import { type SubscriptionPlan } from '@/types/subscriptions'; + +import PlanQuotaDiffList from './PlanQuotaDiffList'; +import * as styles from './index.module.scss'; + +type Props = { + currentPlan: SubscriptionPlan; + targetPlan: SubscriptionPlan; +}; + +function DowngradeConfirmModalContent({ currentPlan, targetPlan }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const { quota: currentQuota, name: currentPlanName } = currentPlan; + const { quota: targetQuota, name: targetPlanName } = targetPlan; + + const currentQuotaDiff = useMemo( + () => diff(targetQuota, currentQuota), + [currentQuota, targetQuota] + ); + + const targetQuotaDiff = useMemo( + () => diff(currentQuota, targetQuota), + [currentQuota, targetQuota] + ); + + return ( +
    +
    + , + currentName: , + }} + > + {t('subscription.downgrade_modal.description')} + +
    +
    + + +
    +
    + ); +} + +export default DowngradeConfirmModalContent; diff --git a/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.module.scss b/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.module.scss new file mode 100644 index 000000000..bb6113335 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.module.scss @@ -0,0 +1,26 @@ +@use '@/scss/underscore' as _; + +.container { + > :not(:first-child) { + margin: _.unit(6) 0 0; + } +} + +.description { + font: var(--font-body-2); +} + +.list { + background-color: var(--color-layer-2); + border-radius: 12px; + padding: _.unit(4); + list-style-position: inside; + + > li:not(:first-child) { + margin-top: _.unit(3); + } +} + +.buttonLink { + text-decoration: none; +} diff --git a/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.tsx new file mode 100644 index 000000000..338c81f76 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/NotEligibleDowngradeModalContent/index.tsx @@ -0,0 +1,83 @@ +import { conditional } from '@silverhand/essentials'; +import { Trans, useTranslation } from 'react-i18next'; + +import PlanName from '@/components/PlanName'; +import DynamicT from '@/ds-components/DynamicT'; +import { type SubscriptionPlan, type SubscriptionPlanQuota } from '@/types/subscriptions'; + +import { quotaItemLimitedPhrasesMap, quotaItemNotEligiblePhrasesMap } from '../quota-item-phrases'; + +import * as styles from './index.module.scss'; + +const excludedQuotaKeys = new Set([ + 'auditLogsRetentionDays', + 'communitySupportEnabled', + 'ticketSupportResponseTime', +]); + +type Props = { + targetPlan: SubscriptionPlan; +}; + +function NotEligibleDowngradeModalContent({ targetPlan }: Props) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const { name, quota } = targetPlan; + + // eslint-disable-next-line no-restricted-syntax + const entries = Object.entries(quota) as Array< + [keyof SubscriptionPlanQuota, SubscriptionPlanQuota[keyof SubscriptionPlanQuota]] + >; + + return ( +
    +
    + , + }} + > + {t('subscription.downgrade_modal.not_eligible_description')} + +
    +
      + {entries.map(([quotaKey, quotaValue]) => { + if ( + excludedQuotaKeys.has(quotaKey) || + quotaValue === null || // Unlimited items + quotaValue === true // Eligible items + ) { + return null; + } + + return ( +
    • + {quotaValue ? ( + + ), + }} + > + {t('subscription.downgrade_modal.a_maximum_of')} + + ) : ( + + )} +
    • + ); + })} +
    +
    + ); +} + +export default NotEligibleDowngradeModalContent; diff --git a/packages/console/src/pages/TenantSettings/Subscription/SwitchPlanActionBar/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/SwitchPlanActionBar/index.tsx index 00ac91f34..90a8f5c4f 100644 --- a/packages/console/src/pages/TenantSettings/Subscription/SwitchPlanActionBar/index.tsx +++ b/packages/console/src/pages/TenantSettings/Subscription/SwitchPlanActionBar/index.tsx @@ -1,9 +1,13 @@ import { contactEmailLink } from '@/consts'; import Button from '@/ds-components/Button'; import Spacer from '@/ds-components/Spacer'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; import { type SubscriptionPlan } from '@/types/subscriptions'; import { isDowngradePlan } from '@/utils/subscription'; +import DowngradeConfirmModalContent from '../DowngradeConfirmModalContent'; +import NotEligibleDowngradeModalContent from '../NotEligibleDowngradeModalContent'; + import * as styles from './index.module.scss'; type Props = { @@ -12,6 +16,39 @@ type Props = { }; function SwitchPlanActionBar({ currentSubscriptionPlanId, subscriptionPlans }: Props) { + const { show } = useConfirmModal(); + + const handleDownGrade = async (targetPlan: SubscriptionPlan) => { + // Todo @xiaoyijun handle downgrade + await show({ + ModalContent: () => , + title: 'subscription.downgrade_modal.not_eligible', + confirmButtonText: 'general.got_it', + confirmButtonType: 'primary', + }); + }; + + const onDowngradeClick = async (targetPlanId: string) => { + const currentPlan = subscriptionPlans.find(({ id }) => id === currentSubscriptionPlanId); + const targetPlan = subscriptionPlans.find(({ id }) => id === targetPlanId); + if (!currentPlan || !targetPlan) { + return; + } + + const [result] = await show({ + ModalContent: () => ( + + ), + title: 'subscription.downgrade_modal.title', + confirmButtonText: 'subscription.downgrade_modal.downgrade', + size: 'large', + }); + + if (result) { + await handleDownGrade(targetPlan); + } + }; + return (
    @@ -32,6 +69,11 @@ function SwitchPlanActionBar({ currentSubscriptionPlanId, subscriptionPlans }: P type={isDowngrade ? 'default' : 'primary'} disabled={isCurrentPlan} onClick={async () => { + if (isDowngrade) { + await onDowngradeClick(planId); + // eslint-disable-next-line no-useless-return + return; + } // Todo @xiaoyijun handle buy plan }} /> diff --git a/packages/console/src/pages/TenantSettings/Subscription/quota-item-phrases.ts b/packages/console/src/pages/TenantSettings/Subscription/quota-item-phrases.ts new file mode 100644 index 000000000..3bc13fd49 --- /dev/null +++ b/packages/console/src/pages/TenantSettings/Subscription/quota-item-phrases.ts @@ -0,0 +1,91 @@ +import { type TFuncKey } from 'i18next'; + +import { type SubscriptionPlanQuota } from '@/types/subscriptions'; + +export const quotaItemPhrasesMap: Record< + keyof SubscriptionPlanQuota, + TFuncKey<'translation', 'admin_console.subscription.quota_item'> +> = { + mauLimit: 'mau_limit.name', + applicationsLimit: 'applications_limit.name', + machineToMachineLimit: 'machine_to_machine_limit.name', + resourcesLimit: 'resources_limit.name', + scopesPerResourceLimit: 'scopes_per_resource_limit.name', + customDomainEnabled: 'custom_domain_enabled.name', + omniSignInEnabled: 'omni_sign_in_enabled.name', + builtInEmailConnectorEnabled: 'built_in_email_connector_enabled.name', + socialConnectorsLimit: 'social_connectors_limit.name', + standardConnectorsLimit: 'standard_connectors_limit.name', + rolesLimit: 'roles_limit.name', + scopesPerRoleLimit: 'scopes_per_role_limit.name', + hooksLimit: 'hooks_limit.name', + auditLogsRetentionDays: 'audit_logs_retention_days.name', + communitySupportEnabled: 'community_support_enabled.name', + ticketSupportResponseTime: 'customer_ticket_support.name', +}; + +export const quotaItemUnlimitedPhrasesMap: Record< + keyof SubscriptionPlanQuota, + TFuncKey<'translation', 'admin_console.subscription.quota_item'> +> = { + mauLimit: 'mau_limit.unlimited', + applicationsLimit: 'applications_limit.unlimited', + machineToMachineLimit: 'machine_to_machine_limit.unlimited', + resourcesLimit: 'resources_limit.unlimited', + scopesPerResourceLimit: 'scopes_per_resource_limit.unlimited', + customDomainEnabled: 'custom_domain_enabled.unlimited', + omniSignInEnabled: 'omni_sign_in_enabled.unlimited', + builtInEmailConnectorEnabled: 'built_in_email_connector_enabled.unlimited', + socialConnectorsLimit: 'social_connectors_limit.unlimited', + standardConnectorsLimit: 'standard_connectors_limit.unlimited', + rolesLimit: 'roles_limit.unlimited', + scopesPerRoleLimit: 'scopes_per_role_limit.unlimited', + hooksLimit: 'hooks_limit.unlimited', + auditLogsRetentionDays: 'audit_logs_retention_days.unlimited', + communitySupportEnabled: 'community_support_enabled.unlimited', + ticketSupportResponseTime: 'customer_ticket_support.unlimited', +}; + +export const quotaItemLimitedPhrasesMap: Record< + keyof SubscriptionPlanQuota, + TFuncKey<'translation', 'admin_console.subscription.quota_item'> +> = { + mauLimit: 'mau_limit.limited', + applicationsLimit: 'applications_limit.limited', + machineToMachineLimit: 'machine_to_machine_limit.limited', + resourcesLimit: 'resources_limit.limited', + scopesPerResourceLimit: 'scopes_per_resource_limit.limited', + customDomainEnabled: 'custom_domain_enabled.limited', + omniSignInEnabled: 'omni_sign_in_enabled.limited', + builtInEmailConnectorEnabled: 'built_in_email_connector_enabled.limited', + socialConnectorsLimit: 'social_connectors_limit.limited', + standardConnectorsLimit: 'standard_connectors_limit.limited', + rolesLimit: 'roles_limit.limited', + scopesPerRoleLimit: 'scopes_per_role_limit.limited', + hooksLimit: 'hooks_limit.limited', + auditLogsRetentionDays: 'audit_logs_retention_days.limited', + communitySupportEnabled: 'community_support_enabled.limited', + ticketSupportResponseTime: 'customer_ticket_support.limited', +}; + +export const quotaItemNotEligiblePhrasesMap: Record< + keyof SubscriptionPlanQuota, + TFuncKey<'translation', 'admin_console.subscription.quota_item'> +> = { + mauLimit: 'mau_limit.not_eligible', + applicationsLimit: 'applications_limit.not_eligible', + machineToMachineLimit: 'machine_to_machine_limit.not_eligible', + resourcesLimit: 'resources_limit.not_eligible', + scopesPerResourceLimit: 'scopes_per_resource_limit.not_eligible', + customDomainEnabled: 'custom_domain_enabled.not_eligible', + omniSignInEnabled: 'omni_sign_in_enabled.not_eligible', + builtInEmailConnectorEnabled: 'built_in_email_connector_enabled.not_eligible', + socialConnectorsLimit: 'social_connectors_limit.not_eligible', + standardConnectorsLimit: 'standard_connectors_limit.not_eligible', + rolesLimit: 'roles_limit.not_eligible', + scopesPerRoleLimit: 'scopes_per_role_limit.not_eligible', + hooksLimit: 'hooks_limit.not_eligible', + auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible', + communitySupportEnabled: 'community_support_enabled.not_eligible', + ticketSupportResponseTime: 'customer_ticket_support.not_eligible', +}; diff --git a/packages/console/src/types/subscriptions.ts b/packages/console/src/types/subscriptions.ts index 65ca10b43..dd741ae63 100644 --- a/packages/console/src/types/subscriptions.ts +++ b/packages/console/src/types/subscriptions.ts @@ -7,7 +7,7 @@ export enum ReservedPlanName { Enterprise = 'Enterprise', } -type SubscriptionPlanQuota = SubscriptionPlanResponse['quota'] & { +export type SubscriptionPlanQuota = SubscriptionPlanResponse['quota'] & { communitySupportEnabled: boolean; ticketSupportResponseTime: number; };