From 69986bc179c642461a2202484684d600ab95a575 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Thu, 12 Dec 2024 14:26:51 +0800 Subject: [PATCH] refactor(console): refactor console paywall guards (#6863) * refactor(console): refactor console paywall guards refactor console paywall guards * refactor(console): refactor the paywall logic refactor the paywall logic * fix(console): replace hardcode pro plan id replace hardcoded pro plan id * chore(console): add some comments add some comments --- .../ApplicationCreation/CreateForm/index.tsx | 8 ++--- .../SkuCardItem/index.tsx | 5 ++-- .../src/components/FeatureTag/index.tsx | 30 ++++++++++++++++--- .../src/components/Guide/GuideCard/index.tsx | 5 ++-- .../src/components/PlanDescription/index.tsx | 1 + .../src/components/PlanUsage/index.tsx | 4 +-- packages/console/src/consts/plan-quotas.ts | 1 + packages/console/src/consts/subscriptions.ts | 5 ++++ .../src/ds-components/CardTitle/index.tsx | 6 ++-- .../components/CreateForm/index.tsx | 8 +++-- .../components/GuideLibrary/index.tsx | 4 +-- .../console/src/pages/CustomizeJwt/index.tsx | 10 ++++--- .../EnterpriseSso/SsoCreationModal/index.tsx | 12 ++++---- .../console/src/pages/EnterpriseSso/index.tsx | 15 ++++------ .../pages/Mfa/MfaForm/UpsellNotice/index.tsx | 8 +++-- .../src/pages/Mfa/PageWrapper/index.tsx | 10 +++---- .../src/pages/OrganizationTemplate/index.tsx | 19 ++++++------ .../CreateOrganizationModal/index.tsx | 14 ++++----- .../console/src/pages/Organizations/index.tsx | 10 +++---- .../Branding/CustomUiForm/index.tsx | 4 +-- .../AddOnUsageChangesNotification/index.tsx | 8 +++-- .../MauLimitExceededNotification/index.tsx | 4 +-- .../TenantMembers/InviteMemberModal/index.tsx | 6 ++-- .../index.tsx | 4 +-- packages/console/src/utils/subscription.ts | 11 ++++++- 25 files changed, 124 insertions(+), 88 deletions(-) diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx index bb6418b12..35ed12a7d 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx @@ -1,6 +1,6 @@ import { type AdminConsoleKey } from '@logto/phrases'; import type { Application } from '@logto/schemas'; -import { ApplicationType, ReservedPlanId } from '@logto/schemas'; +import { ApplicationType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { type ReactElement, useContext, useMemo } from 'react'; import { useController, useForm } from 'react-hook-form'; @@ -11,6 +11,7 @@ import { useSWRConfig } from 'swr'; import { GtagConversionId, reportConversion } from '@/components/Conversion/utils'; import { isDevFeaturesEnabled } from '@/consts/env'; +import { latestProPlanId } from '@/consts/subscriptions'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import DynamicT from '@/ds-components/DynamicT'; import FormField from '@/ds-components/FormField'; @@ -142,10 +143,7 @@ function CreateForm({ title="applications.create" subtitle={subtitleElement} paywall={conditional( - isPaidTenant && - watch('type') === ApplicationType.MachineToMachine && - planId !== ReservedPlanId.Pro && - ReservedPlanId.Pro + !isPaidTenant && watch('type') === ApplicationType.MachineToMachine && latestProPlanId )} hasAddOnTag={ isPaidTenant && diff --git a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/SkuCardItem/index.tsx b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/SkuCardItem/index.tsx index c59e4a044..040e22790 100644 --- a/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/SkuCardItem/index.tsx +++ b/packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/SkuCardItem/index.tsx @@ -14,6 +14,7 @@ import DangerousRaw from '@/ds-components/DangerousRaw'; import DynamicT from '@/ds-components/DynamicT'; import FlipOnRtl from '@/ds-components/FlipOnRtl'; import TextLink from '@/ds-components/TextLink'; +import { isProPlan } from '@/utils/subscription'; import FeaturedSkuContent from './FeaturedSkuContent'; import styles from './index.module.scss'; @@ -92,9 +93,7 @@ function SkuCardItem({ sku, onSelect, buttonProps }: Props) { disabled={(isFreeSku && isFreeTenantExceeded) || buttonProps?.disabled} /> - {skuId === ReservedPlanId.Pro && ( -
{t('most_popular')}
- )} + {isProPlan(skuId) &&
{t('most_popular')}
} ); } diff --git a/packages/console/src/components/FeatureTag/index.tsx b/packages/console/src/components/FeatureTag/index.tsx index 2a6947d48..f1f5f835c 100644 --- a/packages/console/src/components/FeatureTag/index.tsx +++ b/packages/console/src/components/FeatureTag/index.tsx @@ -5,11 +5,29 @@ import { useContext } from 'react'; import { isCloud } from '@/consts/env'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { TenantsContext } from '@/contexts/TenantsProvider'; +import { isProPlan } from '@/utils/subscription'; import styles from './index.module.scss'; export { default as BetaTag } from './BetaTag'; +/** + * The display tag mapping for each ReservedPlanId. + */ +const planIdTagMap: Record = { + [ReservedPlanId.Free]: 'free', + [ReservedPlanId.Pro]: 'pro', + [ReservedPlanId.Pro202411]: 'pro', + [ReservedPlanId.Development]: 'dev', + [ReservedPlanId.Admin]: 'admin', +}; + +/** + * The minimum plan required to use the feature. + * Currently we only have pro plan paywall. + */ +export type PaywallPlanId = Extract; + export type Props = { /** * Whether the tag should be visible. It should be `true` if the tenant's subscription @@ -17,8 +35,12 @@ export type Props = { * tenants. */ readonly isVisible: boolean; - /** The minimum plan required to use the feature. */ - readonly plan: Exclude; + /** + * The minimum plan required to use the feature. + * Currently we only have pro plan paywall. + * Set the default value to the latest pro plan id we are using. + */ + readonly plan: PaywallPlanId; readonly className?: string; }; @@ -61,7 +83,7 @@ function FeatureTag(props: Props) { return null; } - return
{plan}
; + return
{planIdTagMap[plan]}
; } export default FeatureTag; @@ -89,7 +111,7 @@ export function CombinedAddOnAndFeatureTag(props: CombinedAddOnAndFeatureTagProp } // Show the "Add-on" tag for Pro plan. - if (hasAddOnTag && isCloud && planId === ReservedPlanId.Pro) { + if (hasAddOnTag && isCloud && isProPlan(planId)) { return (
Add-on
); diff --git a/packages/console/src/components/Guide/GuideCard/index.tsx b/packages/console/src/components/Guide/GuideCard/index.tsx index 22e491712..e4a8a0945 100644 --- a/packages/console/src/components/Guide/GuideCard/index.tsx +++ b/packages/console/src/components/Guide/GuideCard/index.tsx @@ -1,9 +1,10 @@ -import { ReservedPlanId, Theme } from '@logto/schemas'; +import { Theme } from '@logto/schemas'; import classNames from 'classnames'; import { Suspense, useCallback } from 'react'; import { type Guide, type GuideMetadata } from '@/assets/docs/guides/types'; import FeatureTag, { BetaTag } from '@/components/FeatureTag'; +import { latestProPlanId } from '@/consts/subscriptions'; import Button from '@/ds-components/Button'; import useTheme from '@/hooks/use-theme'; import { onKeyDownHandler } from '@/utils/a11y'; @@ -61,7 +62,7 @@ function GuideCard({ data, onClick, hasBorder, hasButton, hasPaywall, isBeta }:
{name}
{hasTags && (
- {hasPaywall && } + {hasPaywall && } {isBeta && }
)} diff --git a/packages/console/src/components/PlanDescription/index.tsx b/packages/console/src/components/PlanDescription/index.tsx index 8af91bf83..b944ae4e1 100644 --- a/packages/console/src/components/PlanDescription/index.tsx +++ b/packages/console/src/components/PlanDescription/index.tsx @@ -9,6 +9,7 @@ const registeredPlanDescriptionPhrasesMap: Record< > = { [ReservedPlanId.Free]: 'free_plan_description', [ReservedPlanId.Pro]: 'pro_plan_description', + [ReservedPlanId.Pro202411]: 'pro_plan_description', }; const getRegisteredPlanDescriptionPhrase = ( diff --git a/packages/console/src/components/PlanUsage/index.tsx b/packages/console/src/components/PlanUsage/index.tsx index 3eff315bc..5014a0783 100644 --- a/packages/console/src/components/PlanUsage/index.tsx +++ b/packages/console/src/components/PlanUsage/index.tsx @@ -12,7 +12,7 @@ import { import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { TenantsContext } from '@/contexts/TenantsProvider'; import DynamicT from '@/ds-components/DynamicT'; -import { formatPeriod, isPaidPlan } from '@/utils/subscription'; +import { formatPeriod, isPaidPlan, isProPlan } from '@/utils/subscription'; import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard'; import styles from './index.module.scss'; @@ -134,7 +134,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) { // Hide the quota notice for Pro plans if the basic quota is 0. // Per current pricing model design, it should apply to `enterpriseSsoLimit`. ...cond( - planId === ReservedPlanId.Pro && + isProPlan(planId) && currentSubscriptionBasicQuota[key] === 0 && { isQuotaNoticeHidden: true, } diff --git a/packages/console/src/consts/plan-quotas.ts b/packages/console/src/consts/plan-quotas.ts index a5ba94407..9d539d4c3 100644 --- a/packages/console/src/consts/plan-quotas.ts +++ b/packages/console/src/consts/plan-quotas.ts @@ -8,6 +8,7 @@ import { type LogtoSkuQuota } from '@/types/skus'; export const ticketSupportResponseTimeMap: Record = { [ReservedPlanId.Free]: 0, [ReservedPlanId.Pro]: 48, + [ReservedPlanId.Pro202411]: 48, }; /** diff --git a/packages/console/src/consts/subscriptions.ts b/packages/console/src/consts/subscriptions.ts index 3ef8e2ac8..c92acac8c 100644 --- a/packages/console/src/consts/subscriptions.ts +++ b/packages/console/src/consts/subscriptions.ts @@ -1,5 +1,7 @@ import { ReservedPlanId } from '@logto/schemas'; +import { isDevFeaturesEnabled } from './env'; + /** * Shared quota limits between the featured plan content in the `CreateTenantModal` and the `PlanComparisonTable`. */ @@ -34,3 +36,6 @@ export const featuredPlanIds: string[] = [ReservedPlanId.Free, ReservedPlanId.Pr export const featuredPlanIdOrder: string[] = [ReservedPlanId.Free, ReservedPlanId.Pro]; export const checkoutStateQueryKey = 'checkout-state'; + +/** The latest pro plan id we are using. TODO: Remove this when we have the new Pro202411 plan released. */ +export const latestProPlanId = isDevFeaturesEnabled ? ReservedPlanId.Pro202411 : ReservedPlanId.Pro; diff --git a/packages/console/src/ds-components/CardTitle/index.tsx b/packages/console/src/ds-components/CardTitle/index.tsx index c5507c274..2c535d579 100644 --- a/packages/console/src/ds-components/CardTitle/index.tsx +++ b/packages/console/src/ds-components/CardTitle/index.tsx @@ -1,10 +1,9 @@ import type { AdminConsoleKey } from '@logto/phrases'; -import { type ReservedPlanId } from '@logto/schemas'; import classNames from 'classnames'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; -import { CombinedAddOnAndFeatureTag } from '@/components/FeatureTag'; +import { CombinedAddOnAndFeatureTag, type PaywallPlanId } from '@/components/FeatureTag'; import type { Props as TextLinkProps } from '@/ds-components/TextLink'; import type DangerousRaw from '../DangerousRaw'; @@ -22,10 +21,9 @@ export type Props = { readonly className?: string; /** * If a paywall tag should be shown next to the title. The value is the plan type. - * * If not provided, no paywall tag will be shown. */ - readonly paywall?: Exclude; + readonly paywall?: PaywallPlanId; readonly hasAddOnTag?: boolean; }; diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx index d944c46ab..f79cd82c8 100644 --- a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx +++ b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx @@ -1,5 +1,5 @@ import { isValidUrl } from '@logto/core-kit'; -import { ReservedPlanId, type Resource } from '@logto/schemas'; +import { type Resource } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useContext } from 'react'; import { useForm } from 'react-hook-form'; @@ -7,6 +7,7 @@ import { toast } from 'react-hot-toast'; import { Trans, useTranslation } from 'react-i18next'; import Modal from 'react-modal'; +import { latestProPlanId } from '@/consts/subscriptions'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import FormField from '@/ds-components/FormField'; import ModalLayout from '@/ds-components/ModalLayout'; @@ -43,6 +44,7 @@ function CreateForm({ onClose }: Props) { const api = useApi(); const { hasReachedLimit } = useApiResourcesUsage(); + const isPaidTenant = isPaidPlan(planId, isEnterprisePlan); const onSubmit = handleSubmit( trySubmitSafe(async (data) => { @@ -69,8 +71,8 @@ function CreateForm({ onClose }: Props) { } onClose={onClose} > diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx index f1a65506c..97314b84f 100644 --- a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx +++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx @@ -1,4 +1,3 @@ -import { ReservedPlanId } from '@logto/schemas'; import { cond } from '@silverhand/essentials'; import classNames from 'classnames'; import { useCallback, useContext, useMemo, useState } from 'react'; @@ -12,6 +11,7 @@ import { type SelectedGuide } from '@/components/Guide/GuideCard'; import GuideCardGroup from '@/components/Guide/GuideCardGroup'; import { useAppGuideMetadata } from '@/components/Guide/hooks'; import { isCloud } from '@/consts/env'; +import { latestProPlanId } from '@/consts/subscriptions'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { CheckboxGroup } from '@/ds-components/Checkbox'; import OverlayScrollbar from '@/ds-components/OverlayScrollbar'; @@ -105,7 +105,7 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, onSelectGuide } isVisible={ currentSubscriptionQuota.thirdPartyApplicationsLimit === 0 } - plan={ReservedPlanId.Pro} + plan={latestProPlanId} /> ), } diff --git a/packages/console/src/pages/CustomizeJwt/index.tsx b/packages/console/src/pages/CustomizeJwt/index.tsx index 5af3f753c..daba148df 100644 --- a/packages/console/src/pages/CustomizeJwt/index.tsx +++ b/packages/console/src/pages/CustomizeJwt/index.tsx @@ -4,12 +4,13 @@ import { useCallback, useContext, useState } from 'react'; import { useTranslation } from 'react-i18next'; import FormCard, { FormCardSkeleton } from '@/components/FormCard'; -import { isCloud } from '@/consts/env'; +import { latestProPlanId } from '@/consts/subscriptions'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { TenantsContext } from '@/contexts/TenantsProvider'; import CardTitle from '@/ds-components/CardTitle'; import FormField from '@/ds-components/FormField'; import useDocumentationUrl from '@/hooks/use-documentation-url'; +import { isPaidPlan } from '@/utils/subscription'; import CreateButton from './CreateButton'; import CustomizerItem from './CustomizerItem'; @@ -23,12 +24,11 @@ function CustomizeJwt() { const { isDevTenant } = useContext(TenantsContext); const { - currentSubscription: { planId }, + currentSubscription: { planId, isEnterprisePlan }, currentSubscriptionQuota: { customJwtEnabled }, } = useContext(SubscriptionDataContext); const { getDocumentationUrl } = useDocumentationUrl(); - const isCustomJwtEnabled = !isCloud || customJwtEnabled; const showPaywall = planId === ReservedPlanId.Free; @@ -38,13 +38,15 @@ function CustomizeJwt() { setDeleteModalTokenType(tokenType); }, []); + const isPaidTenant = isPaidPlan(planId, isEnterprisePlan); + const { isLoading, accessTokenJwtCustomizer, clientCredentialsJwtCustomizer } = useJwtCustomizer(); return (
0 || - planId === ReservedPlanId.Pro; + currentSubscriptionQuota.enterpriseSsoLimit > 0; + const isPaidTenant = isPaidPlan(planId, isEnterprisePlan); const { data, error } = useSWR( @@ -155,7 +154,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) { > ) ) ?? - (isSsoEnabled ? ( + // Paid tenant can create SSO connectors + (isSsoEnabled || isPaidTenant ? (