0
Fork 0
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:
simeng-li 2024-11-25 17:48:52 +08:00 committed by GitHub
parent 3e034b580f
commit 4b5db6ed1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 55 additions and 101 deletions

View file

@ -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 })}

View file

@ -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', {

View file

@ -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}>

View file

@ -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')}

View file

@ -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;

View file

@ -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;

View file

@ -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)} />

View file

@ -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', {

View file

@ -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', {

View file

@ -205,7 +205,7 @@ function ProtectedAppForm({
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <SkuName skuId={planId} isEnterprisePlan={isEnterprisePlan} />,
planName: <SkuName skuId={planId} />,
}}
>
{t('upsell.paywall.applications', {

View file

@ -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', {

View file

@ -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 */}

View file

@ -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 })}

View file

@ -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">

View file

@ -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>

View file

@ -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', {

View file

@ -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({

View file

@ -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.