mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
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
This commit is contained in:
parent
788270b5a7
commit
69986bc179
25 changed files with 124 additions and 88 deletions
|
@ -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 &&
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
{skuId === ReservedPlanId.Pro && (
|
||||
<div className={styles.mostPopularTag}>{t('most_popular')}</div>
|
||||
)}
|
||||
{isProPlan(skuId) && <div className={styles.mostPopularTag}>{t('most_popular')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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, string> = {
|
||||
[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<ReservedPlanId, ReservedPlanId.Pro | ReservedPlanId.Pro202411>;
|
||||
|
||||
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<ReservedPlanId, ReservedPlanId.Free | ReservedPlanId.Development>;
|
||||
/**
|
||||
* 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 <div className={classNames(styles.tag, className)}>{plan}</div>;
|
||||
return <div className={classNames(styles.tag, className)}>{planIdTagMap[plan]}</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={classNames(styles.tag, styles.beta, styles.addOn, className)}>Add-on</div>
|
||||
);
|
||||
|
|
|
@ -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 }:
|
|||
<div className={styles.name}>{name}</div>
|
||||
{hasTags && (
|
||||
<div className={styles.tagWrapper}>
|
||||
{hasPaywall && <FeatureTag isVisible plan={ReservedPlanId.Pro} />}
|
||||
{hasPaywall && <FeatureTag isVisible plan={latestProPlanId} />}
|
||||
{isBeta && <BetaTag />}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -9,6 +9,7 @@ const registeredPlanDescriptionPhrasesMap: Record<
|
|||
> = {
|
||||
[ReservedPlanId.Free]: 'free_plan_description',
|
||||
[ReservedPlanId.Pro]: 'pro_plan_description',
|
||||
[ReservedPlanId.Pro202411]: 'pro_plan_description',
|
||||
};
|
||||
|
||||
const getRegisteredPlanDescriptionPhrase = (
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { type LogtoSkuQuota } from '@/types/skus';
|
|||
export const ticketSupportResponseTimeMap: Record<string, number> = {
|
||||
[ReservedPlanId.Free]: 0,
|
||||
[ReservedPlanId.Pro]: 48,
|
||||
[ReservedPlanId.Pro202411]: 48,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ReservedPlanId, ReservedPlanId.Free | ReservedPlanId.Development>;
|
||||
readonly paywall?: PaywallPlanId;
|
||||
readonly hasAddOnTag?: boolean;
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
|||
<ModalLayout
|
||||
title="api_resources.create"
|
||||
subtitle="api_resources.subtitle"
|
||||
paywall={conditional(planId !== ReservedPlanId.Pro && ReservedPlanId.Pro)}
|
||||
hasAddOnTag={isPaidPlan(planId, isEnterprisePlan) && hasReachedLimit}
|
||||
paywall={conditional(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidTenant && hasReachedLimit}
|
||||
footer={<Footer isCreationLoading={isSubmitting} onClickCreate={onSubmit} />}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<main className={styles.mainContent}>
|
||||
<CardTitle
|
||||
paywall={cond((!isCustomJwtEnabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={cond(!isPaidTenant && latestProPlanId)}
|
||||
title="jwt_claims.title"
|
||||
subtitle="jwt_claims.description"
|
||||
learnMoreLink={{
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
ReservedPlanId,
|
||||
type RequestErrorBody,
|
||||
type SsoConnectorProvidersResponse,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
|
@ -19,7 +18,7 @@ import { getConnectorRadioGroupSize } from '@/components/CreateConnectorForm/uti
|
|||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { addOnPricingExplanationLink } from '@/consts/external-links';
|
||||
import { enterpriseSsoAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { enterpriseSsoAddOnUnitPrice, latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
@ -63,8 +62,8 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
const isSsoEnabled =
|
||||
!isCloud ||
|
||||
currentSubscriptionQuota.enterpriseSsoLimit === null ||
|
||||
currentSubscriptionQuota.enterpriseSsoLimit > 0 ||
|
||||
planId === ReservedPlanId.Pro;
|
||||
currentSubscriptionQuota.enterpriseSsoLimit > 0;
|
||||
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
const { data, error } = useSWR<SsoConnectorProvidersResponse, RequestError>(
|
||||
|
@ -155,7 +154,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
>
|
||||
<ModalLayout
|
||||
title="enterprise_sso.create_modal.title"
|
||||
paywall={conditional(isPaidTenant && planId !== ReservedPlanId.Pro && ReservedPlanId.Pro)}
|
||||
paywall={conditional(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidTenant}
|
||||
footer={
|
||||
conditional(
|
||||
|
@ -185,7 +184,8 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
</AddOnNoticeFooter>
|
||||
)
|
||||
) ??
|
||||
(isSsoEnabled ? (
|
||||
// Paid tenant can create SSO connectors
|
||||
(isSsoEnabled || isPaidTenant ? (
|
||||
<Button
|
||||
title="enterprise_sso.create_modal.create_button_text"
|
||||
type="primary"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type SsoConnectorWithProviderConfig, ReservedPlanId } from '@logto/schemas';
|
||||
import { type SsoConnectorWithProviderConfig } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -11,9 +11,8 @@ import EnterpriseSsoConnectorEmpty from '@/assets/images/sso-connector-empty.svg
|
|||
import ItemPreview from '@/components/ItemPreview';
|
||||
import ListPage from '@/components/ListPage';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
||||
import Tag from '@/ds-components/Tag';
|
||||
|
@ -36,19 +35,14 @@ function EnterpriseSso() {
|
|||
const { pathname } = useLocation();
|
||||
const { navigate } = useTenantPathname();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const {
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
|
||||
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const isSsoEnabled =
|
||||
!isCloud || currentSubscriptionQuota.enterpriseSsoLimit !== 0 || planId === ReservedPlanId.Pro;
|
||||
|
||||
const url = buildUrl('api/sso-connectors', {
|
||||
page: String(page),
|
||||
page_size: String(pageSize),
|
||||
|
@ -60,14 +54,15 @@ function EnterpriseSso() {
|
|||
|
||||
const isLoading = !data && !error;
|
||||
const [ssoConnectors, totalCount] = data ?? [];
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
return (
|
||||
<ListPage
|
||||
title={{
|
||||
paywall: conditional((!isSsoEnabled || isDevTenant) && ReservedPlanId.Pro),
|
||||
paywall: conditional(!isPaidTenant && latestProPlanId),
|
||||
title: 'enterprise_sso.title',
|
||||
subtitle: 'enterprise_sso.subtitle',
|
||||
hasAddOnTag: isPaidPlan(planId, isEnterprisePlan),
|
||||
hasAddOnTag: isPaidTenant,
|
||||
}}
|
||||
pageMeta={{ titleKey: 'enterprise_sso.page_title' }}
|
||||
createButton={conditional(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -8,6 +7,7 @@ import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
|||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { isPaidPlan } from '@/utils/subscription';
|
||||
|
||||
type Props = {
|
||||
readonly className?: string;
|
||||
|
@ -16,14 +16,16 @@ type Props = {
|
|||
function UpsellNotice({ className }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
data: { mfaUpsellNoticeAcknowledged },
|
||||
update,
|
||||
} = useUserPreferences();
|
||||
|
||||
if (planId !== ReservedPlanId.Pro || mfaUpsellNoticeAcknowledged) {
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
if (!isPaidTenant || mfaUpsellNoticeAcknowledged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
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 { isPaidPlan } from '@/utils/subscription';
|
||||
|
||||
|
@ -16,18 +14,18 @@ type Props = {
|
|||
};
|
||||
|
||||
function PageWrapper({ children }: Props) {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const {
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
currentSubscriptionQuota: { mfaEnabled },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const isMfaEnabled = !isCloud || mfaEnabled || planId === ReservedPlanId.Pro;
|
||||
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PageMeta titleKey="mfa.title" />
|
||||
<CardTitle
|
||||
paywall={cond((!isMfaEnabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={cond(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidPlan(planId, isEnterprisePlan)}
|
||||
title="mfa.title"
|
||||
subtitle="mfa.description"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
|
@ -11,8 +10,8 @@ import PageMeta from '@/components/PageMeta';
|
|||
import { OrganizationTemplateTabs, organizationTemplateLink } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
import { latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
|
@ -22,7 +21,7 @@ import TablePlaceholder from '@/ds-components/Table/TablePlaceholder';
|
|||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import pageLayout from '@/scss/page-layout.module.scss';
|
||||
import { isFeatureEnabled } from '@/utils/subscription';
|
||||
import { isFeatureEnabled, isPaidPlan } from '@/utils/subscription';
|
||||
|
||||
import Introduction from '../Organizations/Introduction';
|
||||
|
||||
|
@ -34,14 +33,16 @@ function OrganizationTemplate() {
|
|||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false);
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
const isOrganizationsDisabled =
|
||||
isCloud &&
|
||||
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||
planId !== ReservedPlanId.Pro;
|
||||
// Check if the organizations feature is disabled except for paid tenants.
|
||||
// Paid tenants can create organizations with organization feature add-on applied to their subscription.
|
||||
isCloud && !isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) && !isPaidTenant;
|
||||
|
||||
const { navigate } = useTenantPathname();
|
||||
|
||||
const handleUpgradePlan = useCallback(() => {
|
||||
|
@ -59,7 +60,7 @@ function OrganizationTemplate() {
|
|||
href: getDocumentationUrl(organizationTemplateLink),
|
||||
targetBlank: 'noopener',
|
||||
}}
|
||||
paywall={cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={cond(!isPaidTenant && latestProPlanId)}
|
||||
/>
|
||||
<Button
|
||||
title="application_details.check_guide"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { type Organization, type CreateOrganization, ReservedPlanId } from '@logto/schemas';
|
||||
import { type Organization, type CreateOrganization } from '@logto/schemas';
|
||||
import { cond, conditional } from '@silverhand/essentials';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
@ -10,7 +10,7 @@ import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
|||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { addOnPricingExplanationLink } from '@/consts/external-links';
|
||||
import { organizationAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { latestProPlanId, organizationAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -41,11 +41,11 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
|||
data: { organizationUpsellNoticeAcknowledged },
|
||||
update,
|
||||
} = useUserPreferences();
|
||||
const isOrganizationsDisabled =
|
||||
isCloud &&
|
||||
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||
planId !== ReservedPlanId.Pro;
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
const isOrganizationsDisabled =
|
||||
// Check if the organizations feature is disabled except for paid tenants.
|
||||
// Paid tenants can create organizations with organization feature add-on applied to their subscription.
|
||||
isCloud && !isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) && !isPaidTenant;
|
||||
|
||||
const {
|
||||
reset,
|
||||
|
@ -82,7 +82,7 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
|||
>
|
||||
<ModalLayout
|
||||
title="organizations.create_organization"
|
||||
paywall={conditional(isPaidTenant && planId !== ReservedPlanId.Pro && ReservedPlanId.Pro)}
|
||||
paywall={conditional(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidTenant}
|
||||
footer={
|
||||
cond(
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
|
||||
|
@ -7,6 +6,7 @@ import PageMeta from '@/components/PageMeta';
|
|||
import { organizationsFeatureLink } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
import { latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
|
@ -35,10 +35,10 @@ function Organizations() {
|
|||
const { navigate } = useTenantPathname();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
const isOrganizationsDisabled =
|
||||
isCloud &&
|
||||
!isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) &&
|
||||
planId !== ReservedPlanId.Pro;
|
||||
isCloud && !isFeatureEnabled(currentSubscriptionQuota.organizationsLimit) && !isPaidTenant;
|
||||
|
||||
const upgradePlan = useCallback(() => {
|
||||
navigate(subscriptionPage);
|
||||
|
@ -63,7 +63,7 @@ function Organizations() {
|
|||
<PageMeta titleKey="organizations.page_title" />
|
||||
<div className={pageLayout.headline}>
|
||||
<CardTitle
|
||||
paywall={cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={cond(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidPlan(planId, isEnterprisePlan)}
|
||||
title="organizations.title"
|
||||
subtitle="organizations.subtitle"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import InlineUpsell from '@/components/InlineUpsell';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Card from '@/ds-components/Card';
|
||||
import CodeEditor from '@/ds-components/CodeEditor';
|
||||
|
@ -88,7 +88,7 @@ function CustomUiForm() {
|
|||
descriptionPosition="top"
|
||||
featureTag={{
|
||||
isVisible: !isBringYourUiEnabled,
|
||||
plan: ReservedPlanId.Pro,
|
||||
plan: latestProPlanId,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -8,6 +7,7 @@ import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
|||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import { isPaidPlan } from '@/utils/subscription';
|
||||
|
||||
type Props = {
|
||||
readonly className?: string;
|
||||
|
@ -16,14 +16,16 @@ type Props = {
|
|||
function AddOnUsageChangesNotification({ className }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
data: { addOnChangesInCurrentCycleNoticeAcknowledged },
|
||||
update,
|
||||
} = useUserPreferences();
|
||||
|
||||
if (planId !== ReservedPlanId.Pro || addOnChangesInCurrentCycleNoticeAcknowledged) {
|
||||
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
|
||||
|
||||
if (!isPaidTenant || addOnChangesInCurrentCycleNoticeAcknowledged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
import { latestProPlanId } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
@ -27,7 +27,7 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
|
|||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const proSku = useMemo(() => logtoSkus.find(({ id }) => id === ReservedPlanId.Pro), [logtoSkus]);
|
||||
const proSku = useMemo(() => logtoSkus.find(({ id }) => id === latestProPlanId), [logtoSkus]);
|
||||
|
||||
const periodicUsage = useMemo(
|
||||
() =>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ReservedPlanId, TenantRole } from '@logto/schemas';
|
||||
import { TenantRole } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
|
@ -9,7 +9,7 @@ import ReactModal from 'react-modal';
|
|||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import { addOnPricingExplanationLink } from '@/consts/external-links';
|
||||
import { tenantMembersAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { latestProPlanId, tenantMembersAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -127,7 +127,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
<ModalLayout
|
||||
size="large"
|
||||
title="tenant_members.invite_modal.title"
|
||||
paywall={conditional(planId !== ReservedPlanId.Pro && ReservedPlanId.Pro)}
|
||||
paywall={conditional(!isPaidTenant && latestProPlanId)}
|
||||
hasAddOnTag={isPaidTenant && hasTenantMembersReachedLimit}
|
||||
subtitle="tenant_members.invite_modal.subtitle"
|
||||
footer={
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import { type LogtoSkuQuota, type LogtoSkuQuotaEntries } from '@/types/skus';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
import { isProPlan } from '@/utils/subscription';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
|
@ -64,7 +64,7 @@ export function NotEligibleSwitchSkuModalContent({
|
|||
>
|
||||
{t(isDowngrade ? 'downgrade_description' : 'upgrade_description')}
|
||||
</Trans>
|
||||
{!isDowngrade && id === ReservedPlanId.Pro && t('upgrade_pro_tip')}
|
||||
{!isDowngrade && isProPlan(id) && t('upgrade_pro_tip')}
|
||||
</div>
|
||||
<ul className={styles.list}>
|
||||
{orderedEntries.map(([quotaKey, quotaValue]) => {
|
||||
|
|
|
@ -102,8 +102,17 @@ export const pickupFeaturedLogtoSkus = (logtoSkus: LogtoSkuResponse[]): LogtoSku
|
|||
logtoSkus.filter(({ id }) => featuredPlanIds.includes(id));
|
||||
|
||||
export const isPaidPlan = (planId: string, isEnterprisePlan: boolean) =>
|
||||
planId === ReservedPlanId.Pro || isEnterprisePlan;
|
||||
isProPlan(planId) || isEnterprisePlan;
|
||||
|
||||
export const isFeatureEnabled = (quota: Nullable<number>): boolean => {
|
||||
return quota === null || quota > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* We may have more than one pro planId in the future.
|
||||
* E.g grandfathered {@link ReservedPlanId.Pro} and new {@link ReservedPlanId.Pro202411}.
|
||||
* User this function to check if the planId can be considered as a pro plan.
|
||||
*/
|
||||
export const isProPlan = (planId: string) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
[ReservedPlanId.Pro, ReservedPlanId.Pro202411].includes(planId as ReservedPlanId);
|
||||
|
|
Loading…
Reference in a new issue