mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor(schemas, console): remove deprecated ReservedPlanIds (#6820)
remove deprecated ReservedPlanIds and refactor the skuId usage in console
This commit is contained in:
parent
3e034b580f
commit
4b5db6ed1c
18 changed files with 55 additions and 101 deletions
|
@ -112,7 +112,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('paywall.applications', { count: currentSubscriptionQuota.applicationsLimit ?? 0 })}
|
||||
|
|
|
@ -29,7 +29,7 @@ function Footer({ isCreatingSocialConnector, isCreateButtonDisabled, onClickCrea
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('social_connectors', {
|
||||
|
|
|
@ -52,7 +52,7 @@ function SkuCardItem({ sku, onSelect, buttonProps }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<PlanDescription skuId={skuId} planId={skuId} />
|
||||
<PlanDescription skuId={skuId} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
|
|
|
@ -21,7 +21,7 @@ import styles from './index.module.scss';
|
|||
|
||||
function MauExceededModal() {
|
||||
const {
|
||||
currentSubscription: { planId, isEnterprisePlan },
|
||||
currentSubscription: { planId },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
||||
|
@ -37,11 +37,12 @@ function MauExceededModal() {
|
|||
return null;
|
||||
}
|
||||
|
||||
const isMauExceeded =
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain, prettier/prettier
|
||||
cond(currentTenant && currentTenant.quota.mauLimit !== null &&
|
||||
currentTenant.usage.activeUsers >= currentTenant.quota.mauLimit
|
||||
);
|
||||
const isMauExceeded = cond(
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
||||
currentTenant &&
|
||||
currentTenant.quota.mauLimit !== null &&
|
||||
currentTenant.usage.activeUsers >= currentTenant.quota.mauLimit
|
||||
);
|
||||
|
||||
if (!isMauExceeded) {
|
||||
return null;
|
||||
|
@ -77,7 +78,7 @@ function MauExceededModal() {
|
|||
<InlineNotification severity="error">
|
||||
<Trans
|
||||
components={{
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.mau_exceeded_modal.notification')}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { type TFuncKey } from 'i18next';
|
||||
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
@ -10,20 +9,26 @@ const registeredPlanDescriptionPhrasesMap: Record<
|
|||
> = {
|
||||
[ReservedPlanId.Free]: 'free_plan_description',
|
||||
[ReservedPlanId.Pro]: 'pro_plan_description',
|
||||
[ReservedPlanId.Enterprise]: 'enterprise_description',
|
||||
};
|
||||
|
||||
const getRegisteredPlanDescriptionPhrase = (
|
||||
skuId: string,
|
||||
isEnterprisePlan = false
|
||||
): TFuncKey<'translation', 'admin_console.subscription'> | undefined => {
|
||||
if (isEnterprisePlan) {
|
||||
return 'enterprise_description';
|
||||
}
|
||||
|
||||
return registeredPlanDescriptionPhrasesMap[skuId];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/** Temporarily mark as optional. */
|
||||
readonly skuId?: string;
|
||||
/** @deprecated */
|
||||
readonly planId: string;
|
||||
readonly skuId: string;
|
||||
readonly isEnterprisePlan?: boolean;
|
||||
};
|
||||
|
||||
function PlanDescription({ skuId, planId }: Props) {
|
||||
const description =
|
||||
conditional(skuId && registeredPlanDescriptionPhrasesMap[skuId]) ??
|
||||
registeredPlanDescriptionPhrasesMap[planId];
|
||||
function PlanDescription({ skuId, isEnterprisePlan = false }: Props) {
|
||||
const description = getRegisteredPlanDescriptionPhrase(skuId, isEnterprisePlan);
|
||||
|
||||
if (!description) {
|
||||
return null;
|
||||
|
|
|
@ -2,37 +2,32 @@ import { ReservedPlanId } from '@logto/schemas';
|
|||
import { type TFuncKey } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ReservedSkuId } from '@/types/subscriptions';
|
||||
|
||||
const registeredSkuIdNamePhraseMap: Record<
|
||||
const registeredPlanNamePhraseMap: Record<
|
||||
string,
|
||||
TFuncKey<'translation', 'admin_console.subscription'> | undefined
|
||||
TFuncKey<'translation', 'admin_console.subscription'>
|
||||
> = {
|
||||
quotaKey: undefined,
|
||||
[ReservedSkuId.Free]: 'free_plan',
|
||||
[ReservedSkuId.Pro]: 'pro_plan',
|
||||
[ReservedSkuId.Development]: 'dev_plan',
|
||||
[ReservedSkuId.Admin]: 'admin_plan',
|
||||
[ReservedSkuId.Enterprise]: 'enterprise',
|
||||
[ReservedPlanId.Free]: 'free_plan',
|
||||
[ReservedPlanId.Pro]: 'pro_plan',
|
||||
[ReservedPlanId.Development]: 'dev_plan',
|
||||
[ReservedPlanId.Admin]: 'admin_plan',
|
||||
} satisfies Record<ReservedPlanId, TFuncKey<'translation', 'admin_console.subscription'>>;
|
||||
|
||||
const getRegisteredSkuNamePhrase = (
|
||||
skuId: string
|
||||
): TFuncKey<'translation', 'admin_console.subscription'> => {
|
||||
const reservedSkuNamePhrase = registeredPlanNamePhraseMap[skuId];
|
||||
|
||||
return reservedSkuNamePhrase ?? 'enterprise';
|
||||
};
|
||||
|
||||
type Props = {
|
||||
readonly skuId: string;
|
||||
readonly isEnterprisePlan?: boolean;
|
||||
};
|
||||
|
||||
function SkuName({ skuId: rawSkuId, isEnterprisePlan = false }: Props) {
|
||||
function SkuName({ skuId }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
|
||||
const skuId = isEnterprisePlan ? ReservedPlanId.Enterprise : rawSkuId;
|
||||
|
||||
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>;
|
||||
const skuNamePhrase = getRegisteredSkuNamePhrase(skuId);
|
||||
return <span>{String(t(skuNamePhrase))}</span>;
|
||||
}
|
||||
|
||||
export default SkuName;
|
||||
|
|
|
@ -41,9 +41,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
|||
<span>{regionName}</span>
|
||||
</div>
|
||||
<span>{t(`tenants.full_env_tag.${tag}`)}</span>
|
||||
{tag !== TenantTag.Development && (
|
||||
<SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />
|
||||
)}
|
||||
{tag !== TenantTag.Development && <SkuName skuId={planId} />}
|
||||
</div>
|
||||
</div>
|
||||
<Tick className={classNames(styles.checkIcon, isSelected && styles.visible)} />
|
||||
|
|
|
@ -85,7 +85,7 @@ function CreatePermissionModal({ resourceId, totalResourceCount, onClose }: Prop
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.scopes_per_resource', {
|
||||
|
|
|
@ -47,7 +47,7 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.resources', {
|
||||
|
|
|
@ -205,7 +205,7 @@ function ProtectedAppForm({
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.applications', {
|
||||
|
|
|
@ -81,7 +81,7 @@ function AssignPermissionsModal({ roleId, roleType, onClose }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.scopes_per_role', {
|
||||
|
|
|
@ -39,7 +39,7 @@ function Footer({ roleType, scopes, isCreating, onClickCreate }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{/* User roles limit paywall */}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
|
@ -65,19 +64,7 @@ function BillingHistory() {
|
|||
{
|
||||
title: <DynamicT forKey="subscription.billing_history.invoice_column" />,
|
||||
dataIndex: 'planName',
|
||||
render: ({ skuId: rawSkuId, periodStart, periodEnd }) => {
|
||||
/**
|
||||
* @remarks
|
||||
* The `skuId` should be either ReservedPlanId.Dev, ReservedPlanId.Pro, ReservedPlanId.Admin, ReservedPlanId.Free, or a random string.
|
||||
* Except for the random string, which corresponds to the custom enterprise plan, other `skuId` values correspond to specific Reserved Plans.
|
||||
*/
|
||||
const skuId =
|
||||
rawSkuId &&
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
(Object.values(ReservedPlanId).includes(rawSkuId as ReservedPlanId)
|
||||
? rawSkuId
|
||||
: ReservedPlanId.Enterprise);
|
||||
|
||||
render: ({ skuId, periodStart, periodEnd }) => {
|
||||
return (
|
||||
<ItemPreview
|
||||
title={formatPeriod({ periodStart, periodEnd, displayYear: true })}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
|
@ -24,7 +23,7 @@ type Props = {
|
|||
|
||||
function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
|
||||
const {
|
||||
currentSku: { id, unitPrice },
|
||||
currentSku: { unitPrice },
|
||||
currentSubscription: { upcomingInvoice, isEnterprisePlan, planId },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
@ -41,8 +40,6 @@ function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
|
|||
[currentTenant, rawPeriodicUsage]
|
||||
);
|
||||
|
||||
const currentSkuId = isEnterprisePlan ? ReservedPlanId.Enterprise : id;
|
||||
|
||||
/**
|
||||
* After the new pricing model goes live, `upcomingInvoice` will always exist. `upcomingInvoice` is updated more frequently than `currentSubscription.upcomingInvoice`.
|
||||
* However, for compatibility reasons, the price of the SKU's corresponding `unitPrice` will be used as a fallback when it does not exist. If `unitPrice` also does not exist, it means that the tenant does not have any applicable paid subscription, and the bill will be 0.
|
||||
|
@ -60,10 +57,10 @@ function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
|
|||
<FormCard title="subscription.current_plan" description="subscription.current_plan_description">
|
||||
<div className={styles.planInfo}>
|
||||
<div className={styles.name}>
|
||||
<SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />
|
||||
<SkuName skuId={planId} />
|
||||
</div>
|
||||
<div className={styles.description}>
|
||||
<PlanDescription skuId={currentSkuId} planId={planId} />
|
||||
<PlanDescription skuId={planId} isEnterprisePlan={isEnterprisePlan} />
|
||||
</div>
|
||||
</div>
|
||||
<FormField title="subscription.plan_usage">
|
||||
|
|
|
@ -124,7 +124,7 @@ function SwitchPlanActionBar({ onSubscriptionUpdated, currentSkuId, logtoSkus }:
|
|||
|
||||
// Let user contact us when they are currently on Enterprise plan. Do not allow users to self-serve downgrade.
|
||||
return isEnterprisePlan ? (
|
||||
<div>
|
||||
<div key={skuId}>
|
||||
<a href={contactEmailLink} className={styles.buttonLink} rel="noopener">
|
||||
<Button title="general.contact_us_action" />
|
||||
</a>
|
||||
|
|
|
@ -68,7 +68,7 @@ function CreateForm({ onClose }: Props) {
|
|||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
|
||||
planName: <SkuName skuId={planId} />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.hooks', {
|
||||
|
|
|
@ -4,19 +4,7 @@ import { type InvoicesResponse } from '@/cloud/types/router';
|
|||
|
||||
export enum ReservedPlanName {
|
||||
Free = 'Free',
|
||||
/** @deprecated */
|
||||
Hobby = 'Hobby',
|
||||
Pro = 'Pro',
|
||||
Enterprise = 'Enterprise',
|
||||
}
|
||||
|
||||
// TODO: use `ReservedPlanId` in the future.
|
||||
export enum ReservedSkuId {
|
||||
Free = 'free',
|
||||
Pro = 'pro',
|
||||
Development = 'dev',
|
||||
Admin = 'admin',
|
||||
Enterprise = 'enterprise',
|
||||
}
|
||||
|
||||
export const localCheckoutSessionGuard = z.object({
|
||||
|
|
|
@ -6,24 +6,7 @@
|
|||
*/
|
||||
export enum ReservedPlanId {
|
||||
Free = 'free',
|
||||
/**
|
||||
* @deprecated
|
||||
* In recent refactoring, the `hobby` plan is now treated as the `pro` plan.
|
||||
* Only use this plan ID to check if a plan is a `pro` plan or not.
|
||||
* This plan ID will be renamed to `pro` after legacy Stripe data is migrated by @darcyYe
|
||||
*
|
||||
* Todo @darcyYe:
|
||||
* - LOG-7846: Rename `hobby` to `pro` and `pro` to `legacy-pro`
|
||||
* - LOG-8339: Migrate legacy Stripe data
|
||||
*/
|
||||
Hobby = 'hobby',
|
||||
Pro = 'pro',
|
||||
Enterprise = 'enterprise',
|
||||
/**
|
||||
* @deprecated
|
||||
* Should not use this plan ID, we only use this tag as a record for the legacy `pro` plan since we will rename the `hobby` plan to be `pro`.
|
||||
*/
|
||||
GrandfatheredPro = 'grandfathered-pro',
|
||||
Development = 'dev',
|
||||
/**
|
||||
* This plan ID is reserved for Admin tenant.
|
||||
|
|
Loading…
Add table
Reference in a new issue