0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor: update display, quota guard and usage report logic for enterprise users (#6565)

* refactor: update display, quota guard and usage report logic for enterprise users

* chore: undo logto email connector dependency update

* chore: use contact us button for pro plan when currently on enterprise plan
This commit is contained in:
Darcy Ye 2024-09-11 20:26:39 +08:00 committed by GitHub
parent cc346b4e0a
commit c368c2799a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 190 additions and 49 deletions

View file

@ -27,7 +27,7 @@
"devDependencies": {
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/cloud": "0.2.5-582d792",
"@logto/cloud": "0.2.5-20fd0a2",
"@logto/connector-kit": "workspace:^4.0.0",
"@logto/core-kit": "workspace:^2.5.0",
"@logto/elements": "workspace:^0.0.0",

View file

@ -13,6 +13,7 @@ import Button from '@/ds-components/Button';
import TextLink from '@/ds-components/TextLink';
import useApplicationsUsage from '@/hooks/use-applications-usage';
import useUserPreferences from '@/hooks/use-user-preferences';
import { isPaidPlan } from '@/utils/subscription';
import styles from './index.module.scss';
@ -26,7 +27,7 @@ type Props = {
function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) {
const {
currentSku,
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell' });
@ -45,7 +46,8 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
selectedType === ApplicationType.MachineToMachine &&
isAddOnAvailable &&
hasMachineToMachineAppsReachedLimit &&
planId === ReservedPlanId.Pro &&
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
isPaidPlan(planId, isEnterprisePlan) &&
!m2mUpsellNoticeAcknowledged
) {
return (

View file

@ -80,9 +80,14 @@ type CombinedAddOnAndFeatureTagProps = {
export function CombinedAddOnAndFeatureTag(props: CombinedAddOnAndFeatureTagProps) {
const { hasAddOnTag, className, paywall } = props;
const {
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
} = useContext(SubscriptionDataContext);
// We believe that the enterprise plan has already allocated sufficient resource quotas in the deal negotiation, so there is no need for upselling, nor will it trigger the add-on tag prompt.
if (isEnterprisePlan) {
return null;
}
// Show the "Add-on" tag for Pro plan when dev features enabled.
if (hasAddOnTag && isAddOnAvailable && isCloud && planId === ReservedPlanId.Pro) {
return (

View file

@ -10,6 +10,7 @@ const registeredPlanDescriptionPhrasesMap: Record<
> = {
[ReservedPlanId.Free]: 'free_plan_description',
[ReservedPlanId.Pro]: 'pro_plan_description',
[ReservedPlanId.Enterprise]: 'enterprise_description',
};
type Props = {

View file

@ -31,6 +31,10 @@
.usageTip {
font: var(--font-body-2);
color: var(--color-text-secondary);
&.hidden {
display: none;
}
}
.tag {

View file

@ -22,6 +22,7 @@ export type Props = {
readonly titleKey: AdminConsoleKey;
readonly tooltipKey?: AdminConsoleKey;
readonly unitPrice: number;
readonly isUsageTipHidden: boolean;
readonly className?: string;
};
@ -32,6 +33,7 @@ function PlanUsageCard({
usageKey,
titleKey,
tooltipKey,
isUsageTipHidden,
className,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -75,7 +77,9 @@ function PlanUsageCard({
>
<Trans
components={{
span: <span className={styles.usageTip} />,
span: (
<span className={classNames(styles.usageTip, isUsageTipHidden && styles.hidden)} />
),
}}
>
{t(usageKey, {

View file

@ -22,13 +22,16 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
const {
currentSubscriptionQuota,
currentSubscriptionUsage,
currentSubscription: currentSubscriptionFromNewPricingModel,
currentSubscription: {
currentPeriodStart,
currentPeriodEnd,
planId,
isAddOnAvailable,
isEnterprisePlan,
},
} = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);
const { currentPeriodStart, currentPeriodEnd, planId, isAddOnAvailable } =
currentSubscriptionFromNewPricingModel;
const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
@ -71,6 +74,8 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
...cond(
(key === 'tokenLimit' || key === 'mauLimit') && { quota: currentSubscriptionQuota[key] }
),
// Hide usage tip for Enterprise plan.
isUsageTipHidden: isEnterprisePlan,
}));
return (
@ -83,7 +88,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
periodStart: currentPeriodStart,
periodEnd: currentPeriodEnd,
}),
renewDate: dayjs(currentPeriodEnd).add(1, 'day').format('MMM D, YYYY'),
renewDate: dayjs(currentPeriodEnd).format('MMM D, YYYY'),
}}
/>
</div>

View file

@ -1,4 +1,4 @@
import { TenantTag } from '@logto/schemas';
import { TenantTag, ReservedPlanId } from '@logto/schemas';
import classNames from 'classnames';
import { useContext, useMemo } from 'react';
@ -23,7 +23,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
const {
name,
tag,
subscription: { planId },
subscription: { planId, isEnterprisePlan },
} = tenantData;
const { logtoSkus } = useContext(SubscriptionDataContext);
@ -31,6 +31,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
() => logtoSkus.find(({ id }) => id === planId),
[logtoSkus, planId]
);
const skuId = isEnterprisePlan ? ReservedPlanId.Enterprise : planId;
if (!tenantSubscriptionSku) {
return null;
@ -48,7 +49,7 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
{tag === TenantTag.Development ? (
<DynamicT forKey="subscription.no_subscription" />
) : (
<SkuName skuId={planId} />
<SkuName skuId={skuId} />
)}
</div>
</div>

View file

@ -31,6 +31,7 @@ export const defaultTenantResponse: TenantResponse = {
planId: defaultSubscriptionPlanId,
currentPeriodStart: dayjs().toDate(),
currentPeriodEnd: dayjs().add(1, 'month').toDate(),
isEnterprisePlan: false,
},
usage: {
activeUsers: 0,

View file

@ -13,6 +13,7 @@ import Button from '@/ds-components/Button';
import TextLink from '@/ds-components/TextLink';
import useApiResourcesUsage from '@/hooks/use-api-resources-usage';
import useUserPreferences from '@/hooks/use-user-preferences';
import { isPaidPlan } from '@/utils/subscription';
import styles from './index.module.scss';
@ -24,7 +25,7 @@ type Props = {
function Footer({ isCreationLoading, onClickCreate }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
currentSubscriptionUsage: { resourcesLimit },
currentSku,
} = useContext(SubscriptionDataContext);
@ -60,7 +61,8 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
if (
isAddOnAvailable &&
hasReachedLimit &&
planId === ReservedPlanId.Pro &&
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
isPaidPlan(planId, isEnterprisePlan) &&
!apiResourceUpsellNoticeAcknowledged
) {
return (

View file

@ -31,6 +31,7 @@ import useApi, { type RequestError } from '@/hooks/use-api';
import useUserPreferences from '@/hooks/use-user-preferences';
import modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
import { isPaidPlan } from '@/utils/subscription';
import SsoConnectorRadioGroup from './SsoConnectorRadioGroup';
import styles from './index.module.scss';
@ -50,7 +51,7 @@ const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name
function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
const {
@ -160,7 +161,8 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
footer={
conditional(
isAddOnAvailable &&
planId === ReservedPlanId.Pro &&
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
isPaidPlan(planId, isEnterprisePlan) &&
!enterpriseSsoUpsellNoticeAcknowledged && (
<AddOnNoticeFooter
buttonTitle="enterprise_sso.create_modal.create_button_text"
@ -178,7 +180,9 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
>
{t('upsell.add_on.footer.enterprise_sso', {
price: enterpriseSsoAddOnUnitPrice,
planName: t('subscription.pro_plan'),
planName: t(
isEnterprisePlan ? 'subscription.enterprise' : 'subscription.pro_plan'
),
})}
</Trans>
</AddOnNoticeFooter>

View file

@ -21,6 +21,7 @@ import useApi from '@/hooks/use-api';
import useUserPreferences from '@/hooks/use-user-preferences';
import modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';
import { isPaidPlan } from '@/utils/subscription';
import styles from './index.module.scss';
@ -33,7 +34,7 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
const api = useApi();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
currentSubscriptionQuota,
} = useContext(SubscriptionDataContext);
const {
@ -85,7 +86,8 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
footer={
cond(
isAddOnAvailable &&
planId === ReservedPlanId.Pro &&
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
isPaidPlan(planId, isEnterprisePlan) &&
!organizationUpsellNoticeAcknowledged && (
<AddOnNoticeFooter
isLoading={isSubmitting}
@ -103,7 +105,9 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
>
{t('upsell.add_on.footer.organization', {
price: organizationAddOnUnitPrice,
planName: t('subscription.pro_plan'),
planName: t(
isEnterprisePlan ? 'subscription.enterprise' : 'subscription.pro_plan'
),
})}
</Trans>
</AddOnNoticeFooter>

View file

@ -1,3 +1,4 @@
import { ReservedPlanId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import dayjs from 'dayjs';
import { useCallback, useContext, useMemo } from 'react';
@ -50,7 +51,19 @@ function BillingHistory() {
{
title: <DynamicT forKey="subscription.billing_history.invoice_column" />,
dataIndex: 'planName',
render: ({ skuId, periodStart, periodEnd }) => {
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);
return (
<ItemPreview
title={formatPeriod({ periodStart, periodEnd, displayYear: true })}

View file

@ -1,3 +1,4 @@
import { ReservedPlanId } from '@logto/schemas';
import { cond } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';
@ -21,7 +22,10 @@ type Props = {
};
function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
const { currentSku, currentSubscription } = useContext(SubscriptionDataContext);
const {
currentSku: { id, unitPrice },
currentSubscription: { upcomingInvoice, isEnterprisePlan, isAddOnAvailable, planId },
} = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);
const periodicUsage = useMemo(
@ -36,13 +40,15 @@ 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.
*/
const upcomingCost = useMemo(
() => currentSubscription.upcomingInvoice?.subtotal ?? currentSku.unitPrice ?? 0,
[currentSku.unitPrice, currentSubscription.upcomingInvoice]
() => upcomingInvoice?.subtotal ?? unitPrice ?? 0,
[unitPrice, upcomingInvoice]
);
if (!periodicUsage) {
@ -53,10 +59,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={currentSku.id} />
<SkuName skuId={currentSkuId} />
</div>
<div className={styles.description}>
<PlanDescription skuId={currentSku.id} planId={currentSubscription.planId} />
<PlanDescription skuId={currentSkuId} planId={planId} />
</div>
</div>
<FormField title="subscription.plan_usage">
@ -65,7 +71,7 @@ function CurrentPlan({ periodicUsage: rawPeriodicUsage }: Props) {
<FormField title="subscription.next_bill">
<BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} />
</FormField>
{currentSubscription.isAddOnAvailable && (
{isAddOnAvailable && !isEnterprisePlan && (
<AddOnUsageChangesNotification className={styles.notification} />
)}
<MauLimitExceedNotification

View file

@ -8,6 +8,7 @@ import { type LogtoSkuResponse } from '@/cloud/types/router';
import SkuName from '@/components/SkuName';
import { contactEmailLink } from '@/consts';
import { subscriptionPage } from '@/consts/pages';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import Spacer from '@/ds-components/Spacer';
@ -29,6 +30,9 @@ type Props = {
function SwitchPlanActionBar({ onSubscriptionUpdated, currentSkuId, logtoSkus }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.subscription' });
const { currentTenantId } = useContext(TenantsContext);
const {
currentSubscription: { isEnterprisePlan },
} = useContext(SubscriptionDataContext);
const { subscribe, cancelSubscription } = useSubscribe();
const { show } = useConfirmModal();
const [currentLoadingSkuId, setCurrentLoadingSkuId] = useState<string>();
@ -118,7 +122,14 @@ function SwitchPlanActionBar({ onSubscriptionUpdated, currentSkuId, logtoSkus }:
const isCurrentSku = currentSkuId === skuId;
const isDowngrade = isDowngradePlan(currentSkuId, skuId);
return (
// Let user contact us for Pro plan when they are currently on Enterprise plan.
return isEnterprisePlan && skuId === ReservedPlanId.Pro ? (
<div>
<a href={contactEmailLink} className={styles.buttonLink} rel="noopener">
<Button title="general.contact_us_action" />
</a>
</div>
) : (
<div key={skuId}>
<Button
title={
@ -140,7 +151,11 @@ function SwitchPlanActionBar({ onSubscriptionUpdated, currentSkuId, logtoSkus }:
})}
<div>
<a href={contactEmailLink} className={styles.buttonLink} rel="noopener">
<Button title="general.contact_us_action" type="primary" />
<Button
title={isEnterprisePlan ? 'subscription.current' : 'general.contact_us_action'}
type="primary"
disabled={isEnterprisePlan}
/>
</a>
</div>
</div>

View file

@ -20,6 +20,7 @@ import { useConfirmModal } from '@/hooks/use-confirm-modal';
import useUserPreferences from '@/hooks/use-user-preferences';
import modalStyles from '@/scss/modal.module.scss';
import { hasReachedSubscriptionQuotaLimit } from '@/utils/quota';
import { isPaidPlan } from '@/utils/subscription';
import InviteEmailsInput from '../InviteEmailsInput';
import useEmailInputUtils from '../InviteEmailsInput/hooks';
@ -42,7 +43,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
const { parseEmailOptions } = useEmailInputUtils();
const { show } = useConfirmModal();
const {
currentSubscription: { planId, isAddOnAvailable },
currentSubscription: { planId, isAddOnAvailable, isEnterprisePlan },
currentSubscriptionQuota,
currentSubscriptionUsage: { tenantMembersLimit },
mutateSubscriptionQuotaAndUsages,
@ -138,7 +139,8 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
conditional(
isAddOnAvailable &&
hasTenantMembersReachedLimit &&
planId === ReservedPlanId.Pro &&
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
isPaidPlan(planId, isEnterprisePlan) &&
!tenantMembersUpsellNoticeAcknowledged && (
<AddOnNoticeFooter
isLoading={isLoading}

View file

@ -1,3 +1,4 @@
import { ReservedPlanId } from '@logto/schemas';
import { conditional, trySafe } from '@silverhand/essentials';
import { ResponseError } from '@withtyped/client';
import dayjs from 'dayjs';
@ -94,3 +95,6 @@ export const parseExceededSkuQuotaLimitError = async (
export const pickupFeaturedLogtoSkus = (logtoSkus: LogtoSkuResponse[]): LogtoSkuResponse[] =>
logtoSkus.filter(({ id }) => featuredPlanIds.includes(id));
export const isPaidPlan = (planId: string, isEnterprisePlan: boolean) =>
planId === ReservedPlanId.Pro || isEnterprisePlan;

View file

@ -97,7 +97,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@logto/cloud": "0.2.5-582d792",
"@logto/cloud": "0.2.5-20fd0a2",
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/adm-zip": "^0.5.5",

View file

@ -17,11 +17,15 @@ export type QuotaLibrary = ReturnType<typeof createQuotaLibrary>;
/**
* @remarks
* Should report usage changes to the Cloud only when the following conditions are met:
* 1. The tenant is on the Pro plan.
* 1. The tenant is either on Pro plan or Enterprise plan.
* 2. The usage key is add-on related usage key.
*/
const shouldReportSubscriptionUpdates = (planId: string, key: keyof SubscriptionQuota) =>
planId === ReservedPlanId.Pro && isReportSubscriptionUpdatesUsageKey(key);
const shouldReportSubscriptionUpdates = (
planId: string,
isEnterprisePlan: boolean,
key: keyof SubscriptionQuota
) =>
(planId === ReservedPlanId.Pro || isEnterprisePlan) && isReportSubscriptionUpdatesUsageKey(key);
export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
const guardTenantUsageByKey = async (key: keyof SubscriptionUsage) => {
@ -41,10 +45,11 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
planId,
quota: fullQuota,
usage: fullUsage,
isEnterprisePlan,
} = await getTenantSubscriptionData(cloudConnection);
// Do not block Pro plan from adding add-on resources.
if (shouldReportSubscriptionUpdates(planId, key)) {
// Do not block Pro/Enterprise plan from adding add-on resources.
if (shouldReportSubscriptionUpdates(planId, isEnterprisePlan, key)) {
return;
}
@ -159,9 +164,11 @@ export const createQuotaLibrary = (cloudConnection: CloudConnectionLibrary) => {
return;
}
const { planId, isAddOnAvailable } = await getTenantSubscriptionData(cloudConnection);
const { planId, isAddOnAvailable, isEnterprisePlan } = await getTenantSubscriptionData(
cloudConnection
);
if (shouldReportSubscriptionUpdates(planId, key) && isAddOnAvailable) {
if (shouldReportSubscriptionUpdates(planId, isEnterprisePlan, key) && isAddOnAvailable) {
await reportSubscriptionUpdates(cloudConnection, key);
}
};

View file

@ -23,6 +23,7 @@ export const getTenantSubscriptionData = async (
cloudConnection: CloudConnectionLibrary
): Promise<{
planId: string;
isEnterprisePlan: boolean;
isAddOnAvailable?: boolean;
quota: SubscriptionQuota;
usage: SubscriptionUsage;
@ -30,12 +31,13 @@ export const getTenantSubscriptionData = async (
roles: Record<string, number>;
}> => {
const client = await cloudConnection.getClient();
const [{ planId, isAddOnAvailable }, { quota, usage, resources, roles }] = await Promise.all([
client.get('/api/tenants/my/subscription'),
client.get('/api/tenants/my/subscription-usage'),
]);
const [{ planId, isAddOnAvailable, isEnterprisePlan }, { quota, usage, resources, roles }] =
await Promise.all([
client.get('/api/tenants/my/subscription'),
client.get('/api/tenants/my/subscription-usage'),
]);
return { planId, isAddOnAvailable, quota, usage, resources, roles };
return { planId, isEnterprisePlan, isAddOnAvailable, quota, usage, resources, roles };
};
export const reportSubscriptionUpdates = async (

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan: 'خطة Pro',
pro_plan_description: 'للاستفادة من الأعمال بدون قلق مع Logto.',
enterprise: 'الشركات',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
current_plan: 'الخطة الحالية',
current_plan_description:
'هذه هي الخطة الحالية الخاصة بك. يمكنك بسهولة رؤية استخدام الخطة الخاصة بك ، والتحقق من فاتورتك القادمة ، وإجراء التغييرات على الخطة حسب الحاجة.',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: 'Für Unternehmen, die sorgenfrei von Logto profitieren möchten.',
enterprise: 'Unternehmen',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,8 @@ const subscription = {
pro_plan: 'Pro plan',
pro_plan_description: 'For businesses benefit worry-free with Logto.',
enterprise: 'Enterprise',
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
admin_plan: 'Admin plan',
dev_plan: 'Development plan',
current_plan: 'Current plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Benefíciese sin preocupaciones con Logto para empresas.',
enterprise: 'Empresa',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Pour les entreprises qui bénéficient de Logto sans soucis.',
enterprise: 'Entreprise',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Per aziende che beneficiano di Logto senza preoccupazioni.',
enterprise: 'Azienda',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'ビジネスが安心してLogtoを利用できるプランです。',
enterprise: 'エンタープライズ',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: 'Logto와 함께 걱정 없이 비즈니스 혜택을 받으세요.',
enterprise: '기업용',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Dla firm, ciesz się bezstresową obsługą Logto.',
enterprise: 'Przedsiębiorstwo',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Para empresas se beneficiarem tranquilo com o Logto.',
enterprise: 'Empresa',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: 'Para empresas que desejam se beneficiar sem preocupações com o Logto.',
enterprise: 'Empresa',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: 'Позволяет бизнесу использовать Logto без забот.',
enterprise: 'Enterprise',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -9,6 +9,9 @@ const subscription = {
pro_plan_description: "Endişesiz bir şekilde Logto'dan faydalanan işletmeler için.",
enterprise: 'Kurumsal',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: '适用于企业付费无忧。',
enterprise: '企业',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: '供企業放心使用Logto。',
enterprise: '企業',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -8,6 +8,9 @@ const subscription = {
pro_plan_description: '企業無憂享受 Logto 服務。',
enterprise: '企業版',
/** UNTRANSLATED */
enterprise_description:
'For large-scale organizations requiring advanced features, full customization, and dedicated support to power mission-critical applications. Tailored to your needs for ultimate security, compliance, and performance.',
/** UNTRANSLATED */
admin_plan: 'Admin plan',
/** UNTRANSLATED */
dev_plan: 'Development plan',

View file

@ -18,6 +18,7 @@ export enum ReservedPlanId {
*/
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`.

View file

@ -2568,8 +2568,8 @@ importers:
specifier: ^29.5.0
version: 29.5.0
'@logto/cloud':
specifier: 0.2.5-582d792
version: 0.2.5-582d792(zod@3.23.8)
specifier: 0.2.5-20fd0a2
version: 0.2.5-20fd0a2(zod@3.23.8)
'@logto/connector-kit':
specifier: workspace:^4.0.0
version: link:../toolkit/connector-kit
@ -3064,8 +3064,8 @@ importers:
version: 3.23.8
devDependencies:
'@logto/cloud':
specifier: 0.2.5-582d792
version: 0.2.5-582d792(zod@3.23.8)
specifier: 0.2.5-20fd0a2
version: 0.2.5-20fd0a2(zod@3.23.8)
'@silverhand/eslint-config':
specifier: 6.0.1
version: 6.0.1(eslint@8.57.0)(prettier@3.0.0)(typescript@5.5.3)
@ -5571,6 +5571,10 @@ packages:
'@logto/client@2.7.2':
resolution: {integrity: sha512-jsmuDl9QpXfR3uLEMPE67tvYoL5XcjJi+4yGqucYPjd4GH6SUHp3N9skk8C/OyygnKDPLY+ttwD0LaIbpGvn+Q==}
'@logto/cloud@0.2.5-20fd0a2':
resolution: {integrity: sha512-j0f2RDpi/OEI59WXKnih7QeFSywNFV91PkulZdmcGa8HCRNmht94siw+LILzheg6bzwfvHU/aN4tJYL1/Px1BA==}
engines: {node: ^20.9.0}
'@logto/cloud@0.2.5-582d792':
resolution: {integrity: sha512-0fIZzqwyjQguTS0a5+XbgVZlGEB/MXIf6pbuBDkHh6JHlMTJ/XH041rWX+e+nMk5N7/Xk2XXS+d2RJUWumnmpw==}
engines: {node: ^20.9.0}
@ -15240,6 +15244,13 @@ snapshots:
camelcase-keys: 7.0.2
jose: 5.6.3
'@logto/cloud@0.2.5-20fd0a2(zod@3.23.8)':
dependencies:
'@silverhand/essentials': 2.9.1
'@withtyped/server': 0.14.0(zod@3.23.8)
transitivePeerDependencies:
- zod
'@logto/cloud@0.2.5-582d792(zod@3.23.8)':
dependencies:
'@silverhand/essentials': 2.9.1