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:
parent
cc346b4e0a
commit
c368c2799a
38 changed files with 190 additions and 49 deletions
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -10,6 +10,7 @@ const registeredPlanDescriptionPhrasesMap: Record<
|
|||
> = {
|
||||
[ReservedPlanId.Free]: 'free_plan_description',
|
||||
[ReservedPlanId.Pro]: 'pro_plan_description',
|
||||
[ReservedPlanId.Enterprise]: 'enterprise_description',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -31,6 +31,10 @@
|
|||
.usageTip {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tag {
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -31,6 +31,7 @@ export const defaultTenantResponse: TenantResponse = {
|
|||
planId: defaultSubscriptionPlanId,
|
||||
currentPeriodStart: dayjs().toDate(),
|
||||
currentPeriodEnd: dayjs().add(1, 'month').toDate(),
|
||||
isEnterprisePlan: false,
|
||||
},
|
||||
usage: {
|
||||
activeUsers: 0,
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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:
|
||||
'هذه هي الخطة الحالية الخاصة بك. يمكنك بسهولة رؤية استخدام الخطة الخاصة بك ، والتحقق من فاتورتك القادمة ، وإجراء التغييرات على الخطة حسب الحاجة.',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue