mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
fix(console): add add-on display issues and refactor component PlanName (#6471)
This commit is contained in:
parent
1c6b9321dd
commit
e0623df01f
32 changed files with 145 additions and 252 deletions
|
@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { addOnPricingExplanationLink } from '@/consts/external-links';
|
||||
import { machineToMachineAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
|
@ -25,9 +25,9 @@ type Props = {
|
|||
|
||||
function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) {
|
||||
const {
|
||||
currentPlan,
|
||||
currentSku,
|
||||
currentSubscription: { isAddOnAvailable },
|
||||
currentSubscription: { planId, isAddOnAvailable },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell' });
|
||||
const {
|
||||
|
@ -41,8 +41,6 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
} = useUserPreferences();
|
||||
|
||||
if (selectedType) {
|
||||
const { id: planId, name: planName, quota } = currentPlan;
|
||||
|
||||
if (
|
||||
selectedType === ApplicationType.MachineToMachine &&
|
||||
isAddOnAvailable &&
|
||||
|
@ -113,10 +111,10 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={planName} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('paywall.applications', { count: quota.applicationsLimit ?? 0 })}
|
||||
{t('paywall.applications', { count: currentSubscriptionQuota.applicationsLimit ?? 0 })}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
);
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useContext } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import { type ConnectorGroup } from '@/types/connector';
|
||||
|
@ -24,7 +24,7 @@ function Footer({
|
|||
onClickCreateButton,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell.paywall' });
|
||||
const { currentPlan, currentSku, currentSubscriptionUsage, currentSubscriptionQuota } =
|
||||
const { currentSku, currentSubscriptionUsage, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
|
||||
const isSocialConnectorsReachLimit = hasReachedSubscriptionQuotaLimit({
|
||||
|
@ -33,16 +33,18 @@ function Footer({
|
|||
quota: currentSubscriptionQuota,
|
||||
});
|
||||
|
||||
if (isCreatingSocialConnector && selectedConnectorGroup) {
|
||||
const { name: planName } = currentPlan;
|
||||
|
||||
if (isSocialConnectorsReachLimit && !selectedConnectorGroup.isStandard) {
|
||||
if (
|
||||
isCreatingSocialConnector &&
|
||||
selectedConnectorGroup &&
|
||||
isSocialConnectorsReachLimit &&
|
||||
!selectedConnectorGroup.isStandard
|
||||
) {
|
||||
return (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={planName} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('social_connectors', {
|
||||
|
@ -52,7 +54,6 @@ function Footer({
|
|||
</QuotaGuardFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import ArrowRight from '@/assets/icons/arrow-right.svg?react';
|
||||
import { type LogtoSkuResponse } from '@/cloud/types/router';
|
||||
import PlanDescription from '@/components/PlanDescription';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { pricingLink } from '@/consts';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button, { type Props as ButtonProps } from '@/ds-components/Button';
|
||||
|
@ -42,7 +42,7 @@ function SkuCardItem({ sku, onSelect, buttonProps }: Props) {
|
|||
<div className={styles.container}>
|
||||
<div className={styles.planInfo}>
|
||||
<div className={styles.title}>
|
||||
<PlanName skuId={skuId} name={skuId} />
|
||||
<SkuName skuId={skuId} />
|
||||
</div>
|
||||
<div className={styles.priceInfo}>
|
||||
<div className={styles.priceLabel}>{t('base_price')}</div>
|
||||
|
@ -77,9 +77,7 @@ function SkuCardItem({ sku, onSelect, buttonProps }: Props) {
|
|||
<Button
|
||||
title={
|
||||
<DangerousRaw>
|
||||
<Trans components={{ name: <PlanName skuId={skuId} name={skuId} /> }}>
|
||||
{t('select_plan')}
|
||||
</Trans>
|
||||
<Trans components={{ name: <SkuName skuId={skuId} /> }}>{t('select_plan')}</Trans>
|
||||
</DangerousRaw>
|
||||
}
|
||||
type={isFreeSku ? 'outline' : 'primary'}
|
||||
|
|
|
@ -35,7 +35,7 @@ function GuideCard({ data, onClick, hasBorder, hasButton }: Props) {
|
|||
} = data;
|
||||
|
||||
const buttonText = target === 'API' ? 'guide.get_started' : 'guide.start_building';
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const theme = useTheme();
|
||||
|
||||
const showPaywallTag = isCloud && isThirdParty;
|
||||
|
@ -73,7 +73,7 @@ function GuideCard({ data, onClick, hasBorder, hasButton }: Props) {
|
|||
<div className={styles.tagWrapper}>
|
||||
{showPaywallTag && (
|
||||
<FeatureTag
|
||||
isVisible={currentPlan.quota.thirdPartyApplicationsLimit === 0}
|
||||
isVisible={currentSubscriptionQuota.thirdPartyApplicationsLimit === 0}
|
||||
plan={ReservedPlanId.Pro}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -15,12 +15,12 @@ import ModalLayout from '@/ds-components/ModalLayout';
|
|||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import PlanName from '../PlanName';
|
||||
import SkuName from '../SkuName';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
function MauExceededModal() {
|
||||
const { currentPlan, currentSubscription, currentSku } = useContext(SubscriptionDataContext);
|
||||
const { currentSku } = useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
@ -35,8 +35,6 @@ function MauExceededModal() {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { name: planName } = currentPlan;
|
||||
|
||||
const isMauExceeded =
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain, prettier/prettier
|
||||
cond(currentTenant && currentTenant.quota.mauLimit !== null &&
|
||||
|
@ -77,7 +75,7 @@ function MauExceededModal() {
|
|||
<InlineNotification severity="error">
|
||||
<Trans
|
||||
components={{
|
||||
planName: <PlanName skuId={currentSku.id} name={planName} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.mau_exceeded_modal.notification')}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import { type TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ReservedPlanName, ReservedSkuId } from '@/types/subscriptions';
|
||||
|
||||
const registeredPlanNamePhraseMap: Record<
|
||||
string,
|
||||
TFuncKey<'translation', 'admin_console.subscription'> | undefined
|
||||
> = {
|
||||
quotaKey: undefined,
|
||||
[ReservedPlanName.Free]: 'free_plan',
|
||||
[ReservedPlanName.Hobby]: 'pro_plan',
|
||||
[ReservedPlanName.Pro]: 'pro_plan',
|
||||
[ReservedPlanName.Enterprise]: 'enterprise',
|
||||
};
|
||||
|
||||
const registeredSkuIdNamePhraseMap: Record<
|
||||
string,
|
||||
TFuncKey<'translation', 'admin_console.subscription'> | undefined
|
||||
> = {
|
||||
quotaKey: undefined,
|
||||
[ReservedSkuId.Free]: 'free_plan',
|
||||
[ReservedSkuId.Pro]: 'pro_plan',
|
||||
[ReservedSkuId.Enterprise]: 'enterprise',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/** Temporarily use optional for backward compatibility. */
|
||||
readonly skuId?: string;
|
||||
/** @deprecated */
|
||||
readonly name: string;
|
||||
};
|
||||
|
||||
// TODO: rename the component once new pricing model is ready, should be `SkuName`.
|
||||
function PlanName({ skuId, name }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
|
||||
const planNamePhrase =
|
||||
conditional(skuId && registeredSkuIdNamePhraseMap[skuId]) ?? registeredPlanNamePhraseMap[name];
|
||||
|
||||
/**
|
||||
* Note: fallback to the plan name if the phrase is not registered.
|
||||
*/
|
||||
const planName = planNamePhrase ? String(t(planNamePhrase)) : name;
|
||||
|
||||
return <span>{planName}</span>;
|
||||
}
|
||||
|
||||
export default PlanName;
|
|
@ -30,7 +30,7 @@
|
|||
max-width: calc((100% - _.unit(2) * 2) / 3);
|
||||
max-height: 112px;
|
||||
|
||||
&.freeUser {
|
||||
&.periodicUsage {
|
||||
flex: 0 0 calc((100% - _.unit(2)) / 2);
|
||||
max-width: calc((100% - _.unit(2)) / 2);
|
||||
}
|
||||
|
|
|
@ -45,12 +45,15 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const onlyShowPeriodicUsage =
|
||||
planId === ReservedPlanId.Free || (!isAddOnAvailable && planId === ReservedPlanId.Pro);
|
||||
|
||||
const usages: PlanUsageCardProps[] = usageKeys
|
||||
// Show all usages for Pro plan and only show MAU and token usage for Free plan
|
||||
.filter(
|
||||
(key) =>
|
||||
isAddOnAvailable ??
|
||||
(planId === ReservedPlanId.Free && (key === 'mauLimit' || key === 'tokenLimit'))
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
isAddOnAvailable || (onlyShowPeriodicUsage && (key === 'mauLimit' || key === 'tokenLimit'))
|
||||
)
|
||||
.map((key) => ({
|
||||
usage:
|
||||
|
@ -89,10 +92,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
|
|||
<PlanUsageCard
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
className={classNames(
|
||||
styles.cardItem,
|
||||
planId === ReservedPlanId.Free && styles.freeUser
|
||||
)}
|
||||
className={classNames(styles.cardItem, onlyShowPeriodicUsage && styles.periodicUsage)}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
|
|
32
packages/console/src/components/SkuName/index.tsx
Normal file
32
packages/console/src/components/SkuName/index.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { type TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ReservedSkuId } from '@/types/subscriptions';
|
||||
|
||||
const registeredSkuIdNamePhraseMap: Record<
|
||||
string,
|
||||
TFuncKey<'translation', 'admin_console.subscription'> | undefined
|
||||
> = {
|
||||
quotaKey: undefined,
|
||||
[ReservedSkuId.Free]: 'free_plan',
|
||||
[ReservedSkuId.Pro]: 'pro_plan',
|
||||
[ReservedSkuId.Enterprise]: 'enterprise',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
readonly skuId: string;
|
||||
};
|
||||
|
||||
function SkuName({ skuId }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
|
||||
const skuNamePhrase = registeredSkuIdNamePhraseMap[skuId];
|
||||
|
||||
/**
|
||||
* Note: fallback to the plan name if the phrase is not registered.
|
||||
*/
|
||||
const skuName = skuNamePhrase ? String(t(skuNamePhrase)) : skuId;
|
||||
|
||||
return <span>{skuName}</span>;
|
||||
}
|
||||
|
||||
export default SkuName;
|
|
@ -4,7 +4,7 @@ import { useContext, useMemo } from 'react';
|
|||
|
||||
import Tick from '@/assets/icons/tick.svg?react';
|
||||
import { type TenantResponse } from '@/cloud/types/router';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import TenantEnvTag from '@/components/TenantEnvTag';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||
|
@ -26,7 +26,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
|||
subscription: { planId },
|
||||
} = tenantData;
|
||||
|
||||
const { currentPlan, subscriptionPlans } = useContext(SubscriptionDataContext);
|
||||
const { subscriptionPlans } = useContext(SubscriptionDataContext);
|
||||
const tenantSubscriptionPlan = useMemo(
|
||||
() => subscriptionPlans.find((plan) => plan.id === planId),
|
||||
[subscriptionPlans, planId]
|
||||
|
@ -48,7 +48,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
|||
{tag === TenantTag.Development ? (
|
||||
<DynamicT forKey="subscription.no_subscription" />
|
||||
) : (
|
||||
<PlanName skuId={planId} name={currentPlan.name} />
|
||||
<SkuName skuId={planId} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
|
||||
import { type LogtoSkuQuota } from '@/types/skus';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
/**
|
||||
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
|
||||
|
@ -37,11 +36,4 @@ export const skuQuotaItemOrder: Array<keyof LogtoSkuQuota> = [
|
|||
'ticketSupportResponseTime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Unreleased quota keys will be added here, and it will effect the following:
|
||||
* - Related quota items will have a "Coming soon" tag in the plan selection component.
|
||||
* - Related quota items will be hidden from the downgrade plan notification modal.
|
||||
*/
|
||||
export const comingSoonQuotaKeys: Array<keyof SubscriptionPlanQuota> = [];
|
||||
|
||||
export const comingSoonSkuQuotaKeys: Array<keyof LogtoSkuQuota> = [];
|
||||
|
|
|
@ -6,8 +6,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import ReactModal from 'react-modal';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -28,12 +28,8 @@ type Props = {
|
|||
type CreatePermissionFormData = Pick<CreateScope, 'name' | 'description'>;
|
||||
|
||||
function CreatePermissionModal({ resourceId, totalResourceCount, onClose }: Props) {
|
||||
const {
|
||||
currentPlan,
|
||||
currentSku,
|
||||
currentSubscriptionQuota,
|
||||
currentSubscriptionResourceScopeUsage,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { currentSku, currentSubscriptionQuota, currentSubscriptionResourceScopeUsage } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const {
|
||||
|
@ -87,7 +83,7 @@ function CreatePermissionModal({ resourceId, totalResourceCount, onClose }: Prop
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={currentPlan.name} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.scopes_per_resource', {
|
||||
|
|
|
@ -4,8 +4,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { addOnPricingExplanationLink } from '@/consts/external-links';
|
||||
import { resourceAddOnUnitPrice } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
|
@ -24,7 +24,6 @@ type Props = {
|
|||
function Footer({ isCreationLoading, onClickCreate }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId, isAddOnAvailable },
|
||||
currentSubscriptionUsage: { resourcesLimit },
|
||||
currentSku,
|
||||
|
@ -47,7 +46,7 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={currentPlan.name} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.resources', {
|
||||
|
|
|
@ -38,7 +38,7 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, onSelectGuide }
|
|||
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
|
||||
const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
||||
const isApplicationCreateModal = pathname.includes('/applications/create');
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const { currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
|
||||
const structuredMetadata = useMemo(
|
||||
() => getStructuredAppGuideMetadata({ categories: filterCategories }),
|
||||
|
@ -98,7 +98,9 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, onSelectGuide }
|
|||
category === 'ThirdParty' && {
|
||||
tag: (
|
||||
<FeatureTag
|
||||
isVisible={currentPlan.quota.thirdPartyApplicationsLimit === 0}
|
||||
isVisible={
|
||||
currentSubscriptionQuota.thirdPartyApplicationsLimit === 0
|
||||
}
|
||||
plan={ReservedPlanId.Pro}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -11,8 +11,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import useSWRImmutable from 'swr/immutable';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button, { type Props as ButtonProps } from '@/ds-components/Button';
|
||||
|
@ -46,11 +46,7 @@ function ProtectedAppForm({
|
|||
onCreateSuccess,
|
||||
}: Props) {
|
||||
const { data } = useSWRImmutable<ProtectedAppsDomainConfig>(isCloud && 'api/systems/application');
|
||||
const {
|
||||
currentPlan: { name: planName },
|
||||
currentSku,
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { currentSku, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const { hasAppsReachedLimit } = useApplicationsUsage();
|
||||
const defaultDomain = data?.protectedApps.defaultDomain ?? '';
|
||||
const { navigate } = useTenantPathname();
|
||||
|
@ -206,7 +202,7 @@ function ProtectedAppForm({
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={planName} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.applications', {
|
||||
|
|
|
@ -10,7 +10,7 @@ import useSWR from 'swr';
|
|||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import { GtagConversionId, reportToGoogle } from '@/components/Conversion/utils';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { checkoutStateQueryKey } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
@ -95,14 +95,7 @@ function CheckoutSuccessCallback() {
|
|||
toast.success(
|
||||
<Trans
|
||||
components={{
|
||||
name: (
|
||||
<PlanName
|
||||
skuId={checkoutSku.id}
|
||||
// Generally `checkoutPlanId` and a properly setup of SKU `name` should not be null, we still need to handle the edge case to make the type inference happy.
|
||||
// Also `name` will be deprecated in the future once the new pricing model is ready.
|
||||
name={checkoutPlanId ?? checkoutSku.name ?? checkoutSku.id}
|
||||
/>
|
||||
),
|
||||
name: <SkuName skuId={checkoutSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t(isDowngrade ? 'downgrade_success' : 'upgrade_success')}
|
||||
|
|
|
@ -155,6 +155,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
paywall={conditional(
|
||||
isAddOnAvailable && planId !== ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
hasAddOnTag={isAddOnAvailable}
|
||||
footer={
|
||||
conditional(
|
||||
isAddOnAvailable &&
|
||||
|
|
|
@ -32,7 +32,7 @@ const basePathname = '/organization-template';
|
|||
function OrganizationTemplate() {
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false);
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const { currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const isOrganizationsDisabled = isCloud && !currentSubscriptionQuota.organizationsEnabled;
|
||||
const { navigate } = useTenantPathname();
|
||||
|
|
|
@ -5,9 +5,9 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import ReactModal from 'react-modal';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -24,7 +24,7 @@ type Props = {
|
|||
|
||||
function AssignPermissionsModal({ roleId, roleType, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentPlan, currentSku, currentSubscriptionRoleScopeUsage, currentSubscriptionQuota } =
|
||||
const { currentSku, currentSubscriptionRoleScopeUsage, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopes, setScopes] = useState<ScopeResponse[]>([]);
|
||||
|
@ -79,7 +79,7 @@ function AssignPermissionsModal({ roleId, roleType, onClose }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={currentPlan.name} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.scopes_per_role', {
|
||||
|
|
|
@ -3,8 +3,8 @@ import { useContext } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import {
|
||||
|
@ -20,7 +20,7 @@ type Props = {
|
|||
|
||||
function Footer({ roleType, isCreating, onClickCreate }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentPlan, currentSku, currentSubscriptionQuota, currentSubscriptionUsage } =
|
||||
const { currentSku, currentSubscriptionQuota, currentSubscriptionUsage } =
|
||||
useContext(SubscriptionDataContext);
|
||||
|
||||
const hasRoleReachedLimit = hasReachedSubscriptionQuotaLimit({
|
||||
|
@ -44,23 +44,23 @@ function Footer({ roleType, isCreating, onClickCreate }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={currentPlan.name} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{/* User roles limit paywall */}
|
||||
{hasRoleReachedLimit &&
|
||||
roleType === RoleType.User &&
|
||||
t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
|
||||
t('upsell.paywall.roles', { count: currentSubscriptionQuota.userRolesLimit ?? 0 })}
|
||||
{hasRoleReachedLimit &&
|
||||
roleType === RoleType.MachineToMachine &&
|
||||
t('upsell.paywall.machine_to_machine_roles', {
|
||||
count: currentPlan.quota.machineToMachineRolesLimit ?? 0,
|
||||
count: currentSubscriptionQuota.machineToMachineRolesLimit ?? 0,
|
||||
})}
|
||||
{/* Role scopes limit paywall */}
|
||||
{!hasRoleReachedLimit &&
|
||||
hasScopesPerRoleSurpassedLimit &&
|
||||
t('upsell.paywall.scopes_per_role', {
|
||||
count: currentPlan.quota.scopesPerRoleLimit ?? 0,
|
||||
count: currentSubscriptionQuota.scopesPerRoleLimit ?? 0,
|
||||
})}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
|
|
|
@ -22,8 +22,8 @@ function CustomUiForm() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const { control } = useFormContext<SignInExperienceForm>();
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const isBringYourUiEnabled = currentPlan.quota.bringYourUiEnabled;
|
||||
const { currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const isBringYourUiEnabled = currentSubscriptionQuota.bringYourUiEnabled;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
|||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||
import ItemPreview from '@/components/ItemPreview';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import Table from '@/ds-components/Table';
|
||||
|
@ -50,11 +50,11 @@ function BillingHistory() {
|
|||
{
|
||||
title: <DynamicT forKey="subscription.billing_history.invoice_column" />,
|
||||
dataIndex: 'planName',
|
||||
render: ({ planName, periodStart, periodEnd }) => {
|
||||
render: ({ skuId, periodStart, periodEnd }) => {
|
||||
return (
|
||||
<ItemPreview
|
||||
title={formatPeriod({ periodStart, periodEnd, displayYear: true })}
|
||||
subtitle={conditional(planName && <PlanName name={planName} />)}
|
||||
subtitle={conditional(skuId && <SkuName skuId={skuId} />)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -6,8 +6,8 @@ import BillInfo from '@/components/BillInfo';
|
|||
import ChargeNotification from '@/components/ChargeNotification';
|
||||
import FormCard from '@/components/FormCard';
|
||||
import PlanDescription from '@/components/PlanDescription';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import PlanUsage from '@/components/PlanUsage';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
@ -23,7 +23,7 @@ type Props = {
|
|||
};
|
||||
|
||||
function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
|
||||
const { currentPlan, currentSku, currentSubscription, currentSubscriptionQuota } =
|
||||
const { currentSku, currentSubscription, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
||||
|
@ -62,7 +62,7 @@ function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
|
|||
<FormCard title="subscription.current_plan" description="subscription.current_plan_description">
|
||||
<div className={styles.planInfo}>
|
||||
<div className={styles.name}>
|
||||
<PlanName skuId={currentSku.id} name={currentPlan.name} />
|
||||
<SkuName skuId={currentSku.id} />
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<PlanDescription skuId={currentSku.id} planId={currentSubscription.planId} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { skuQuotaItemOrder } from '@/consts/plan-quotas';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import { type LogtoSkuQuota, type LogtoSkuQuotaEntries } from '@/types/skus';
|
||||
|
@ -11,12 +11,12 @@ import PlanQuotaList from './PlanQuotaList';
|
|||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly planName: string;
|
||||
readonly skuId: string;
|
||||
readonly skuQuotaDiff: Partial<LogtoSkuQuota>;
|
||||
readonly isDowngradeTargetPlan?: boolean;
|
||||
};
|
||||
|
||||
function PlanQuotaDiffCard({ planName, skuQuotaDiff, isDowngradeTargetPlan = false }: Props) {
|
||||
function PlanQuotaDiffCard({ skuId, skuQuotaDiff, isDowngradeTargetPlan = false }: Props) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const sortedSkuQuotaEntries = useMemo(
|
||||
() =>
|
||||
|
@ -33,7 +33,7 @@ function PlanQuotaDiffCard({ planName, skuQuotaDiff, isDowngradeTargetPlan = fal
|
|||
<div className={styles.title}>
|
||||
<Trans
|
||||
components={{
|
||||
name: <PlanName name={planName} />,
|
||||
name: <SkuName skuId={skuId} />,
|
||||
}}
|
||||
>
|
||||
<DynamicT
|
||||
|
|
|
@ -3,33 +3,18 @@ import { useMemo } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { type LogtoSkuResponse } from '@/cloud/types/router';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import { comingSoonQuotaKeys, comingSoonSkuQuotaKeys } from '@/consts/plan-quotas';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { comingSoonSkuQuotaKeys } from '@/consts/plan-quotas';
|
||||
import { type LogtoSkuQuota, type LogtoSkuQuotaEntries } from '@/types/skus';
|
||||
import {
|
||||
type SubscriptionPlanQuota,
|
||||
type SubscriptionPlan,
|
||||
type SubscriptionPlanQuotaEntries,
|
||||
} from '@/types/subscriptions';
|
||||
|
||||
import PlanQuotaDiffCard from './PlanQuotaDiffCard';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly currentPlan: SubscriptionPlan;
|
||||
readonly targetPlan: SubscriptionPlan;
|
||||
readonly currentSku: LogtoSkuResponse;
|
||||
readonly targetSku: LogtoSkuResponse;
|
||||
};
|
||||
|
||||
const excludeComingSoonFeatures = (
|
||||
quotaDiff: Partial<SubscriptionPlanQuota>
|
||||
): Partial<SubscriptionPlanQuota> => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const entries = Object.entries(quotaDiff) as SubscriptionPlanQuotaEntries;
|
||||
return Object.fromEntries(entries.filter(([key]) => !comingSoonQuotaKeys.includes(key)));
|
||||
};
|
||||
|
||||
const excludeSkuComingSoonFeatures = (
|
||||
quotaDiff: Partial<LogtoSkuQuota>
|
||||
): Partial<LogtoSkuQuota> => {
|
||||
|
@ -38,28 +23,14 @@ const excludeSkuComingSoonFeatures = (
|
|||
return Object.fromEntries(entries.filter(([key]) => !comingSoonSkuQuotaKeys.includes(key)));
|
||||
};
|
||||
|
||||
function DowngradeConfirmModalContent({ currentPlan, targetPlan, currentSku, targetSku }: Props) {
|
||||
function DowngradeConfirmModalContent({ currentSku, targetSku }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { quota: currentQuota, name: currentPlanName } = currentPlan;
|
||||
|
||||
const { quota: targetQuota, name: targetPlanName } = targetPlan;
|
||||
|
||||
const currentQuotaDiff = useMemo(
|
||||
() => excludeComingSoonFeatures(diff(targetQuota, currentQuota)),
|
||||
[currentQuota, targetQuota]
|
||||
);
|
||||
|
||||
const currentSkuQuotaDiff = useMemo(
|
||||
() => excludeSkuComingSoonFeatures(diff(targetSku.quota, currentSku.quota)),
|
||||
[targetSku.quota, currentSku.quota]
|
||||
);
|
||||
|
||||
const targetQuotaDiff = useMemo(
|
||||
() => excludeComingSoonFeatures(diff(currentQuota, targetQuota)),
|
||||
[currentQuota, targetQuota]
|
||||
);
|
||||
|
||||
const targetSkuQuotaDiff = useMemo(
|
||||
() => excludeSkuComingSoonFeatures(diff(currentSku.quota, targetSku.quota)),
|
||||
[targetSku.quota, currentSku.quota]
|
||||
|
@ -70,18 +41,18 @@ function DowngradeConfirmModalContent({ currentPlan, targetPlan, currentSku, tar
|
|||
<div className={styles.description}>
|
||||
<Trans
|
||||
components={{
|
||||
targetName: <PlanName skuId={targetSku.id} name={targetPlanName} />,
|
||||
currentName: <PlanName skuId={currentSku.id} name={currentPlanName} />,
|
||||
targetName: <SkuName skuId={targetSku.id} />,
|
||||
currentName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('subscription.downgrade_modal.description')}
|
||||
</Trans>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<PlanQuotaDiffCard planName={currentPlanName} skuQuotaDiff={currentSkuQuotaDiff} />
|
||||
<PlanQuotaDiffCard skuId={currentSku.id} skuQuotaDiff={currentSkuQuotaDiff} />
|
||||
<PlanQuotaDiffCard
|
||||
isDowngradeTargetPlan
|
||||
planName={targetPlanName}
|
||||
skuId={targetSku.id}
|
||||
skuQuotaDiff={targetSkuQuotaDiff}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type LogtoSkuResponse } from '@/cloud/types/router';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { contactEmailLink } from '@/consts';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
@ -14,7 +14,6 @@ import Spacer from '@/ds-components/Spacer';
|
|||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import useSubscribe from '@/hooks/use-subscribe';
|
||||
import { NotEligibleSwitchSkuModalContent } from '@/pages/TenantSettings/components/NotEligibleSwitchPlanModalContent';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
import { isDowngradePlan, parseExceededSkuQuotaLimitError } from '@/utils/subscription';
|
||||
|
||||
import DowngradeConfirmModalContent from '../DowngradeConfirmModalContent';
|
||||
|
@ -22,51 +21,34 @@ import DowngradeConfirmModalContent from '../DowngradeConfirmModalContent';
|
|||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly currentSubscriptionPlanId: string;
|
||||
readonly subscriptionPlans: SubscriptionPlan[];
|
||||
readonly currentSkuId: string;
|
||||
readonly logtoSkus: LogtoSkuResponse[];
|
||||
readonly onSubscriptionUpdated: () => Promise<void>;
|
||||
};
|
||||
|
||||
function SwitchPlanActionBar({
|
||||
currentSubscriptionPlanId,
|
||||
subscriptionPlans,
|
||||
onSubscriptionUpdated,
|
||||
currentSkuId,
|
||||
logtoSkus,
|
||||
}: Props) {
|
||||
function SwitchPlanActionBar({ onSubscriptionUpdated, currentSkuId, logtoSkus }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { subscribe, cancelSubscription } = useSubscribe();
|
||||
const { show } = useConfirmModal();
|
||||
const [currentLoadingPlanId, setCurrentLoadingPlanId] = useState<string>();
|
||||
const [currentLoadingSkuId, setCurrentLoadingSkuId] = useState<string>();
|
||||
|
||||
const handleSubscribe = async (targetSkuId: string, isDowngrade: boolean) => {
|
||||
if (currentLoadingPlanId) {
|
||||
if (currentLoadingSkuId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: clear plan related use cases.
|
||||
const currentPlan = subscriptionPlans.find(({ id }) => id === currentSubscriptionPlanId);
|
||||
const targetPlan = subscriptionPlans.find(({ id }) => id === targetSkuId);
|
||||
|
||||
const currentSku = logtoSkus.find(({ id }) => id === currentSkuId);
|
||||
const targetSku = logtoSkus.find(({ id }) => id === targetSkuId);
|
||||
|
||||
if (!currentPlan || !targetPlan || !currentSku || !targetSku) {
|
||||
if (!currentSku || !targetSku) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDowngrade) {
|
||||
const [result] = await show({
|
||||
ModalContent: () => (
|
||||
<DowngradeConfirmModalContent
|
||||
currentPlan={currentPlan}
|
||||
targetPlan={targetPlan}
|
||||
currentSku={currentSku}
|
||||
targetSku={targetSku}
|
||||
/>
|
||||
<DowngradeConfirmModalContent currentSku={currentSku} targetSku={targetSku} />
|
||||
),
|
||||
title: 'subscription.downgrade_modal.title',
|
||||
confirmButtonText: 'subscription.downgrade_modal.downgrade',
|
||||
|
@ -79,12 +61,12 @@ function SwitchPlanActionBar({
|
|||
}
|
||||
|
||||
try {
|
||||
setCurrentLoadingPlanId(targetSkuId);
|
||||
setCurrentLoadingSkuId(targetSkuId);
|
||||
if (targetSkuId === ReservedPlanId.Free) {
|
||||
await cancelSubscription(currentTenantId);
|
||||
await onSubscriptionUpdated();
|
||||
toast.success(
|
||||
<Trans components={{ name: <PlanName skuId={targetSku.id} name={targetPlan.name} /> }}>
|
||||
<Trans components={{ name: <SkuName skuId={targetSku.id} /> }}>
|
||||
{t('downgrade_success')}
|
||||
</Trans>
|
||||
);
|
||||
|
@ -100,7 +82,7 @@ function SwitchPlanActionBar({
|
|||
callbackPage: subscriptionPage,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setCurrentLoadingPlanId(undefined);
|
||||
setCurrentLoadingSkuId(undefined);
|
||||
|
||||
const [result, exceededSkuQuotaKeys] = await parseExceededSkuQuotaLimitError(error);
|
||||
|
||||
|
@ -125,7 +107,7 @@ function SwitchPlanActionBar({
|
|||
|
||||
void toastResponseError(error);
|
||||
} finally {
|
||||
setCurrentLoadingPlanId(undefined);
|
||||
setCurrentLoadingSkuId(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -148,7 +130,7 @@ function SwitchPlanActionBar({
|
|||
}
|
||||
type={isDowngrade ? 'default' : 'primary'}
|
||||
disabled={isCurrentSku}
|
||||
isLoading={!isCurrentSku && currentLoadingPlanId === skuId}
|
||||
isLoading={!isCurrentSku && currentLoadingSkuId === skuId}
|
||||
onClick={() => {
|
||||
void handleSubscribe(skuId, isDowngrade);
|
||||
}}
|
||||
|
|
|
@ -6,7 +6,7 @@ import PageMeta from '@/components/PageMeta';
|
|||
import { isCloud } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import { pickupFeaturedLogtoSkus, pickupFeaturedPlans } from '@/utils/subscription';
|
||||
import { pickupFeaturedLogtoSkus } from '@/utils/subscription';
|
||||
|
||||
import Skeleton from '../components/Skeleton';
|
||||
|
||||
|
@ -17,16 +17,10 @@ import styles from './index.module.scss';
|
|||
|
||||
function Subscription() {
|
||||
const cloudApi = useCloudApi();
|
||||
const {
|
||||
subscriptionPlans,
|
||||
logtoSkus,
|
||||
currentSku,
|
||||
currentSubscription,
|
||||
onCurrentSubscriptionUpdated,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { logtoSkus, currentSku, onCurrentSubscriptionUpdated } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
|
||||
const reservedPlans = pickupFeaturedPlans(subscriptionPlans);
|
||||
const reservedSkus = pickupFeaturedLogtoSkus(logtoSkus);
|
||||
|
||||
const { data: periodicUsage, isLoading } = useSWR(
|
||||
|
@ -53,8 +47,6 @@ function Subscription() {
|
|||
<CurrentPlan periodicUsage={periodicUsage} />
|
||||
<PlanComparisonTable />
|
||||
<SwitchPlanActionBar
|
||||
currentSubscriptionPlanId={currentSubscription.planId}
|
||||
subscriptionPlans={reservedPlans}
|
||||
currentSkuId={currentSku.id}
|
||||
logtoSkus={reservedSkus}
|
||||
onSubscriptionUpdated={async () => {
|
||||
|
|
|
@ -21,8 +21,7 @@ type Props = {
|
|||
function Footer({ newInvitationCount = 0, isLoading, onSubmit }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell.paywall' });
|
||||
|
||||
const { currentPlan, currentSku } = useContext(SubscriptionDataContext);
|
||||
const { quota } = currentPlan;
|
||||
const { currentSku, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
|
||||
const { hasTenantMembersReachedLimit, limit, usage } = useTenantMembersUsage();
|
||||
|
||||
|
@ -48,7 +47,7 @@ function Footer({ newInvitationCount = 0, isLoading, onSubmit }: Props) {
|
|||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t('tenant_members_dev_plan', { limit: quota.tenantMembersLimit })}
|
||||
{t('tenant_members_dev_plan', { limit: currentSubscriptionQuota.tenantMembersLimit })}
|
||||
</div>
|
||||
<LinkButton
|
||||
size="large"
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import { type LogtoSkuResponse } from '@/cloud/types/router';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { skuQuotaItemOrder } from '@/consts/plan-quotas';
|
||||
import {
|
||||
skuQuotaItemLimitedPhrasesMap,
|
||||
|
@ -41,7 +41,7 @@ export function NotEligibleSwitchSkuModalContent({
|
|||
keyPrefix: 'admin_console.subscription.not_eligible_modal',
|
||||
});
|
||||
|
||||
const { id, name, quota } = targetSku;
|
||||
const { id, quota } = targetSku;
|
||||
|
||||
const orderedEntries = useMemo(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -59,7 +59,7 @@ export function NotEligibleSwitchSkuModalContent({
|
|||
<div className={styles.description}>
|
||||
<Trans
|
||||
components={{
|
||||
name: <PlanName skuId={id} name={name ?? id} />,
|
||||
name: <SkuName skuId={id} />,
|
||||
}}
|
||||
>
|
||||
{t(isDowngrade ? 'downgrade_description' : 'upgrade_description')}
|
||||
|
|
|
@ -5,8 +5,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
|
||||
import BasicWebhookForm, { type BasicWebhookFormType } from '@/components/BasicWebhookForm';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import SkuName from '@/components/SkuName';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
|
@ -26,7 +26,7 @@ type CreateHookPayload = Pick<CreateHook, 'name'> & {
|
|||
};
|
||||
|
||||
function CreateForm({ onClose }: Props) {
|
||||
const { currentPlan, currentSku, currentSubscriptionQuota, currentSubscriptionUsage } =
|
||||
const { currentSku, currentSubscriptionQuota, currentSubscriptionUsage } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
|
@ -70,7 +70,7 @@ function CreateForm({ onClose }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <PlanName skuId={currentSku.id} name={currentPlan.name} />,
|
||||
planName: <SkuName skuId={currentSku.id} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.hooks', {
|
||||
|
|
|
@ -17,7 +17,7 @@ export enum ReservedSkuId {
|
|||
Enterprise = 'enterprise',
|
||||
}
|
||||
|
||||
export type SubscriptionPlanQuota = Omit<
|
||||
type SubscriptionPlanQuota = Omit<
|
||||
SubscriptionPlanResponse['quota'],
|
||||
'builtInEmailConnectorEnabled'
|
||||
> & {
|
||||
|
@ -25,10 +25,6 @@ export type SubscriptionPlanQuota = Omit<
|
|||
ticketSupportResponseTime: number;
|
||||
};
|
||||
|
||||
export type SubscriptionPlanQuotaEntries = Array<
|
||||
[keyof SubscriptionPlanQuota, SubscriptionPlanQuota[keyof SubscriptionPlanQuota]]
|
||||
>;
|
||||
|
||||
export type SubscriptionPlan = Omit<SubscriptionPlanResponse, 'quota'> & {
|
||||
quota: SubscriptionPlanQuota;
|
||||
};
|
||||
|
|
|
@ -7,7 +7,6 @@ import { type LogtoSkuResponse, type SubscriptionPlanResponse } from '@/cloud/ty
|
|||
import { ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
|
||||
import { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions';
|
||||
import { type LogtoSkuQuota } from '@/types/skus';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlanResponse) => {
|
||||
const { id, quota } = subscriptionPlanResponse;
|
||||
|
@ -93,8 +92,5 @@ export const parseExceededSkuQuotaLimitError = async (
|
|||
return [true, Object.keys(exceededQuota) as Array<keyof LogtoSkuQuota>];
|
||||
};
|
||||
|
||||
export const pickupFeaturedPlans = (plans: SubscriptionPlan[]): SubscriptionPlan[] =>
|
||||
plans.filter(({ id }) => featuredPlanIds.includes(id));
|
||||
|
||||
export const pickupFeaturedLogtoSkus = (logtoSkus: LogtoSkuResponse[]): LogtoSkuResponse[] =>
|
||||
logtoSkus.filter(({ id }) => featuredPlanIds.includes(id));
|
||||
|
|
Loading…
Reference in a new issue