0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(console): separate subscription based usage (#6448)

* refactor(console): separate subscription based usage

* refactor: add periodic usage fallback to avoid breaking changes

* fix: fix mock tenant data
This commit is contained in:
Darcy Ye 2024-08-15 12:20:10 +08:00 committed by GitHub
parent db42279ed4
commit 87ff8cb8af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 148 additions and 36 deletions

View file

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

View file

@ -22,10 +22,14 @@ export type NewSubscriptionUsageResponse = GuardedResponse<
/** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */ /** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */
export type NewSubscriptionQuota = NewSubscriptionUsageResponse['quota']; export type NewSubscriptionQuota = NewSubscriptionUsageResponse['quota'];
/** The response of `GET /api/tenants/my/subscription/usage` has the same response type. */ /** The response of `GET /api/tenants/my/subscription/usage` has the same response type. */
export type NewSubscriptionUsage = NewSubscriptionUsageResponse['usage']; export type NewSubscriptionCountBasedUsage = NewSubscriptionUsageResponse['usage'];
export type NewSubscriptionResourceScopeUsage = NewSubscriptionUsageResponse['resources']; export type NewSubscriptionResourceScopeUsage = NewSubscriptionUsageResponse['resources'];
export type NewSubscriptionRoleScopeUsage = NewSubscriptionUsageResponse['roles']; export type NewSubscriptionRoleScopeUsage = NewSubscriptionUsageResponse['roles'];
export type NewSubscriptionPeriodicUsage = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription/periodic-usage']
>;
/* ===== Use `New` in the naming to avoid confusion with legacy types ===== */ /* ===== Use `New` in the naming to avoid confusion with legacy types ===== */
export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>; export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;

View file

@ -1,3 +1,4 @@
import { cond } from '@silverhand/essentials';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
@ -6,6 +7,7 @@ import PlanUsage from '@/components/PlanUsage';
import { contactEmailLink } from '@/consts'; import { contactEmailLink } from '@/consts';
import { subscriptionPage } from '@/consts/pages'; import { subscriptionPage } from '@/consts/pages';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button'; import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification'; import InlineNotification from '@/ds-components/InlineNotification';
@ -18,13 +20,8 @@ import PlanName from '../PlanName';
import styles from './index.module.scss'; import styles from './index.module.scss';
function MauExceededModal() { function MauExceededModal() {
const { const { currentPlan, currentSubscription, currentSku } = useContext(SubscriptionDataContext);
currentPlan, const { currentTenant } = useContext(TenantsContext);
currentSubscription,
currentSku,
currentSubscriptionQuota,
currentSubscriptionUsage,
} = useContext(SubscriptionDataContext);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { navigate } = useTenantPathname(); const { navigate } = useTenantPathname();
@ -41,8 +38,10 @@ function MauExceededModal() {
const { name: planName } = currentPlan; const { name: planName } = currentPlan;
const isMauExceeded = const isMauExceeded =
currentSubscriptionQuota.mauLimit !== null && // eslint-disable-next-line @typescript-eslint/prefer-optional-chain, prettier/prettier
currentSubscriptionUsage.mauLimit >= currentSubscriptionQuota.mauLimit; cond(currentTenant && currentTenant.quota.mauLimit !== null &&
currentTenant.usage.activeUsers >= currentTenant.quota.mauLimit
);
if (!isMauExceeded) { if (!isMauExceeded) {
return null; return null;

View file

@ -2,11 +2,12 @@ import { ReservedPlanId } from '@logto/schemas';
import { cond, conditional } from '@silverhand/essentials'; import { cond, conditional } from '@silverhand/essentials';
import classNames from 'classnames'; import classNames from 'classnames';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useContext } from 'react'; import { useContext, useMemo } from 'react';
import { type Subscription } from '@/cloud/types/router'; import { type Subscription, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
import { isDevFeaturesEnabled } from '@/consts/env'; import { isDevFeaturesEnabled } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT'; import DynamicT from '@/ds-components/DynamicT';
import { type SubscriptionPlan } from '@/types/subscriptions'; import { type SubscriptionPlan } from '@/types/subscriptions';
import { formatPeriod } from '@/utils/subscription'; import { formatPeriod } from '@/utils/subscription';
@ -20,28 +21,49 @@ type Props = {
readonly currentSubscription: Subscription; readonly currentSubscription: Subscription;
/** @deprecated */ /** @deprecated */
readonly currentPlan: SubscriptionPlan; readonly currentPlan: SubscriptionPlan;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
}; };
function PlanUsage({ currentSubscription, currentPlan }: Props) { function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodicUsage }: Props) {
const { const {
currentSubscriptionQuota, currentSubscriptionQuota,
currentSubscriptionUsage, currentSubscriptionUsage,
currentSubscription: currentSubscriptionFromNewPricingModel, currentSubscription: currentSubscriptionFromNewPricingModel,
} = useContext(SubscriptionDataContext); } = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);
const { currentPeriodStart, currentPeriodEnd } = isDevFeaturesEnabled const { currentPeriodStart, currentPeriodEnd } = isDevFeaturesEnabled
? currentSubscriptionFromNewPricingModel ? currentSubscriptionFromNewPricingModel
: currentSubscription; : currentSubscription;
const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant, rawPeriodicUsage]
);
if (!periodicUsage) {
return null;
}
const [activeUsers, mauLimit] = [ const [activeUsers, mauLimit] = [
currentSubscriptionUsage.mauLimit, periodicUsage.mauLimit,
isDevFeaturesEnabled ? currentSubscriptionQuota.mauLimit : currentPlan.quota.mauLimit, isDevFeaturesEnabled ? currentSubscriptionQuota.mauLimit : currentPlan.quota.mauLimit,
]; ];
const usagePercent = conditional(mauLimit && activeUsers / mauLimit); const usagePercent = conditional(mauLimit && activeUsers / mauLimit);
const usages: ProPlanUsageCardProps[] = usageKeys.map((key) => ({ const usages: ProPlanUsageCardProps[] = usageKeys.map((key) => ({
usage: currentSubscriptionUsage[key], usage:
key === 'mauLimit' || key === 'tokenLimit'
? periodicUsage[key]
: currentSubscriptionUsage[key],
usageKey: `subscription.usage.${usageKeyMap[key]}`, usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`, titleKey: `subscription.usage.${titleKeyMap[key]}`,
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`, tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,

View file

@ -5,7 +5,7 @@ import {
type NewSubscriptionQuota, type NewSubscriptionQuota,
type LogtoSkuResponse, type LogtoSkuResponse,
type TenantResponse, type TenantResponse,
type NewSubscriptionUsage, type NewSubscriptionCountBasedUsage,
} from '@/cloud/types/router'; } from '@/cloud/types/router';
import { RegionName } from '@/components/Region'; import { RegionName } from '@/components/Region';
import { LogtoSkuType } from '@/types/skus'; import { LogtoSkuType } from '@/types/skus';
@ -34,9 +34,11 @@ export const defaultTenantResponse: TenantResponse = {
}, },
usage: { usage: {
activeUsers: 0, activeUsers: 0,
tokenUsage: 0,
}, },
quota: { quota: {
mauLimit: null, mauLimit: null,
tokenLimit: null,
}, },
openInvoices: [], openInvoices: [],
isSuspended: false, isSuspended: false,
@ -143,9 +145,7 @@ export const defaultSubscriptionQuota: NewSubscriptionQuota = {
bringYourUiEnabled: false, bringYourUiEnabled: false,
}; };
export const defaultSubscriptionUsage: NewSubscriptionUsage = { export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = {
mauLimit: 0,
tokenLimit: 0,
applicationsLimit: 0, applicationsLimit: 0,
machineToMachineLimit: 0, machineToMachineLimit: 0,
resourcesLimit: 0, resourcesLimit: 0,

View file

@ -2,7 +2,7 @@ import {
type LogtoSkuResponse, type LogtoSkuResponse,
type Subscription, type Subscription,
type NewSubscriptionQuota, type NewSubscriptionQuota,
type NewSubscriptionUsage, type NewSubscriptionCountBasedUsage,
type NewSubscriptionResourceScopeUsage, type NewSubscriptionResourceScopeUsage,
type NewSubscriptionRoleScopeUsage, type NewSubscriptionRoleScopeUsage,
} from '@/cloud/types/router'; } from '@/cloud/types/router';
@ -21,7 +21,7 @@ type NewSubscriptionSupplementContext = {
logtoSkus: LogtoSkuResponse[]; logtoSkus: LogtoSkuResponse[];
currentSku: LogtoSkuResponse; currentSku: LogtoSkuResponse;
currentSubscriptionQuota: NewSubscriptionQuota; currentSubscriptionQuota: NewSubscriptionQuota;
currentSubscriptionUsage: NewSubscriptionUsage; currentSubscriptionUsage: NewSubscriptionCountBasedUsage;
currentSubscriptionResourceScopeUsage: NewSubscriptionResourceScopeUsage; currentSubscriptionResourceScopeUsage: NewSubscriptionResourceScopeUsage;
currentSubscriptionRoleScopeUsage: NewSubscriptionRoleScopeUsage; currentSubscriptionRoleScopeUsage: NewSubscriptionRoleScopeUsage;
mutateSubscriptionQuotaAndUsages: () => void; mutateSubscriptionQuotaAndUsages: () => void;

View file

@ -1,7 +1,9 @@
import { ReservedPlanId } from '@logto/schemas'; import { ReservedPlanId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo, useState } from 'react'; import { useContext, useMemo, useState } from 'react';
import { toastResponseError } from '@/cloud/hooks/use-cloud-api'; import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
import { type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
import { isDevFeaturesEnabled } from '@/consts/env'; import { isDevFeaturesEnabled } from '@/consts/env';
import { subscriptionPage } from '@/consts/pages'; import { subscriptionPage } from '@/consts/pages';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
@ -23,14 +25,20 @@ type Props = {
/** @deprecated No need to pass in this argument in new pricing model */ /** @deprecated No need to pass in this argument in new pricing model */
readonly currentPlan: SubscriptionPlan; readonly currentPlan: SubscriptionPlan;
readonly className?: string; readonly className?: string;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
}; };
function MauLimitExceededNotification({ currentPlan, className }: Props) { function MauLimitExceededNotification({
currentPlan,
periodicUsage: rawPeriodicUsage,
className,
}: Props) {
const { currentTenantId } = useContext(TenantsContext); const { currentTenantId } = useContext(TenantsContext);
const { subscribe } = useSubscribe(); const { subscribe } = useSubscribe();
const { show } = useConfirmModal(); const { show } = useConfirmModal();
const { subscriptionPlans, logtoSkus, currentSubscriptionQuota, currentSubscriptionUsage } = const { subscriptionPlans, logtoSkus, currentSubscriptionQuota } =
useContext(SubscriptionDataContext); useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const proPlan = useMemo( const proPlan = useMemo(
@ -43,6 +51,22 @@ function MauLimitExceededNotification({ currentPlan, className }: Props) {
quota: { mauLimit: oldPricingModelMauLimit }, quota: { mauLimit: oldPricingModelMauLimit },
} = currentPlan; } = currentPlan;
const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant, rawPeriodicUsage]
);
if (!periodicUsage) {
return null;
}
// Should be safe to access `mauLimit` here since we have excluded the case where `isDevFeaturesEnabled` is `true` but `currentSubscriptionQuota` is `null` in the above condition. // Should be safe to access `mauLimit` here since we have excluded the case where `isDevFeaturesEnabled` is `true` but `currentSubscriptionQuota` is `null` in the above condition.
const mauLimit = isDevFeaturesEnabled const mauLimit = isDevFeaturesEnabled
? currentSubscriptionQuota.mauLimit ? currentSubscriptionQuota.mauLimit
@ -50,7 +74,7 @@ function MauLimitExceededNotification({ currentPlan, className }: Props) {
if ( if (
mauLimit === null || // Unlimited mauLimit === null || // Unlimited
currentSubscriptionUsage.mauLimit < mauLimit || periodicUsage.mauLimit < mauLimit ||
!proPlan || !proPlan ||
!proSku !proSku
) { ) {

View file

@ -1,7 +1,7 @@
import { cond } from '@silverhand/essentials'; import { cond, conditional } from '@silverhand/essentials';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { type Subscription } from '@/cloud/types/router'; import { type Subscription, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
import BillInfo from '@/components/BillInfo'; import BillInfo from '@/components/BillInfo';
import ChargeNotification from '@/components/ChargeNotification'; import ChargeNotification from '@/components/ChargeNotification';
import FormCard from '@/components/FormCard'; import FormCard from '@/components/FormCard';
@ -10,6 +10,7 @@ import PlanName from '@/components/PlanName';
import PlanUsage from '@/components/PlanUsage'; import PlanUsage from '@/components/PlanUsage';
import { isDevFeaturesEnabled } from '@/consts/env'; import { isDevFeaturesEnabled } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import FormField from '@/ds-components/FormField'; import FormField from '@/ds-components/FormField';
import { type SubscriptionPlan } from '@/types/subscriptions'; import { type SubscriptionPlan } from '@/types/subscriptions';
import { hasSurpassedQuotaLimit, hasSurpassedSubscriptionQuotaLimit } from '@/utils/quota'; import { hasSurpassedQuotaLimit, hasSurpassedSubscriptionQuotaLimit } from '@/utils/quota';
@ -23,10 +24,12 @@ type Props = {
readonly subscription: Subscription; readonly subscription: Subscription;
/** @deprecated */ /** @deprecated */
readonly subscriptionPlan: SubscriptionPlan; readonly subscriptionPlan: SubscriptionPlan;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
}; };
function CurrentPlan({ subscription, subscriptionPlan }: Props) { function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodicUsage }: Props) {
const { currentSku, currentSubscription, currentSubscriptionUsage, currentSubscriptionQuota } = const { currentTenant } = useContext(TenantsContext);
const { currentSku, currentSubscription, currentSubscriptionQuota } =
useContext(SubscriptionDataContext); useContext(SubscriptionDataContext);
const { const {
id, id,
@ -34,6 +37,18 @@ function CurrentPlan({ subscription, subscriptionPlan }: Props) {
quota: { tokenLimit }, quota: { tokenLimit },
} = subscriptionPlan; } = subscriptionPlan;
const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant, rawPeriodicUsage]
);
/** /**
* After the new pricing model goes live, `upcomingInvoice` will always exist. 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. * After the new pricing model goes live, `upcomingInvoice` will always exist. 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.
*/ */
@ -42,15 +57,19 @@ function CurrentPlan({ subscription, subscriptionPlan }: Props) {
[currentSku.unitPrice, currentSubscription.upcomingInvoice?.subtotal] [currentSku.unitPrice, currentSubscription.upcomingInvoice?.subtotal]
); );
if (!periodicUsage) {
return null;
}
const hasTokenSurpassedLimit = isDevFeaturesEnabled const hasTokenSurpassedLimit = isDevFeaturesEnabled
? hasSurpassedSubscriptionQuotaLimit({ ? hasSurpassedSubscriptionQuotaLimit({
quotaKey: 'tokenLimit', quotaKey: 'tokenLimit',
usage: currentSubscriptionUsage.tokenLimit, usage: periodicUsage.tokenLimit,
quota: currentSubscriptionQuota, quota: currentSubscriptionQuota,
}) })
: hasSurpassedQuotaLimit({ : hasSurpassedQuotaLimit({
quotaKey: 'tokenLimit', quotaKey: 'tokenLimit',
usage: currentSubscriptionUsage.tokenLimit, usage: periodicUsage.tokenLimit,
plan: subscriptionPlan, plan: subscriptionPlan,
}); });
@ -65,12 +84,20 @@ function CurrentPlan({ subscription, subscriptionPlan }: Props) {
</div> </div>
</div> </div>
<FormField title="subscription.plan_usage"> <FormField title="subscription.plan_usage">
<PlanUsage currentSubscription={subscription} currentPlan={subscriptionPlan} /> <PlanUsage
currentSubscription={subscription}
currentPlan={subscriptionPlan}
periodicUsage={rawPeriodicUsage}
/>
</FormField> </FormField>
<FormField title="subscription.next_bill"> <FormField title="subscription.next_bill">
<BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} /> <BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} />
</FormField> </FormField>
<MauLimitExceedNotification currentPlan={subscriptionPlan} className={styles.notification} /> <MauLimitExceedNotification
currentPlan={subscriptionPlan}
periodicUsage={rawPeriodicUsage}
className={styles.notification}
/>
<ChargeNotification <ChargeNotification
hasSurpassedLimit={hasTokenSurpassedLimit} hasSurpassedLimit={hasTokenSurpassedLimit}
quotaItemPhraseKey="tokens" quotaItemPhraseKey="tokens"

View file

@ -1,7 +1,12 @@
import { useContext } from 'react'; import { useContext } from 'react';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import PageMeta from '@/components/PageMeta'; import PageMeta from '@/components/PageMeta';
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
import { Skeleton } from '@/containers/ConsoleContent/Sidebar';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import { pickupFeaturedLogtoSkus, pickupFeaturedPlans } from '@/utils/subscription'; import { pickupFeaturedLogtoSkus, pickupFeaturedPlans } from '@/utils/subscription';
import CurrentPlan from './CurrentPlan'; import CurrentPlan from './CurrentPlan';
@ -10,6 +15,7 @@ import SwitchPlanActionBar from './SwitchPlanActionBar';
import styles from './index.module.scss'; import styles from './index.module.scss';
function Subscription() { function Subscription() {
const cloudApi = useCloudApi();
const { const {
subscriptionPlans, subscriptionPlans,
currentPlan, currentPlan,
@ -18,14 +24,33 @@ function Subscription() {
currentSubscription, currentSubscription,
onCurrentSubscriptionUpdated, onCurrentSubscriptionUpdated,
} = useContext(SubscriptionDataContext); } = useContext(SubscriptionDataContext);
const { currentTenantId } = useContext(TenantsContext);
const reservedPlans = pickupFeaturedPlans(subscriptionPlans); const reservedPlans = pickupFeaturedPlans(subscriptionPlans);
const reservedSkus = pickupFeaturedLogtoSkus(logtoSkus); const reservedSkus = pickupFeaturedLogtoSkus(logtoSkus);
const { data: periodicUsage, isLoading } = useSWR(
isCloud &&
isDevFeaturesEnabled &&
`/api/tenants/${currentTenantId}/subscription/periodic-usage`,
async () =>
cloudApi.get(`/api/tenants/:tenantId/subscription/periodic-usage`, {
params: { tenantId: currentTenantId },
})
);
if (isLoading) {
return <Skeleton />;
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
<PageMeta titleKey={['tenants.tabs.subscription', 'tenants.title']} /> <PageMeta titleKey={['tenants.tabs.subscription', 'tenants.title']} />
<CurrentPlan subscription={currentSubscription} subscriptionPlan={currentPlan} /> <CurrentPlan
subscription={currentSubscription}
subscriptionPlan={currentPlan}
periodicUsage={periodicUsage}
/>
<PlanComparisonTable /> <PlanComparisonTable />
<SwitchPlanActionBar <SwitchPlanActionBar
currentSubscriptionPlanId={currentSubscription.planId} currentSubscriptionPlanId={currentSubscription.planId}

View file

@ -2458,8 +2458,8 @@ importers:
specifier: ^29.5.0 specifier: ^29.5.0
version: 29.5.0 version: 29.5.0
'@logto/cloud': '@logto/cloud':
specifier: 0.2.5-a6cff75 specifier: 0.2.5-9a1b047
version: 0.2.5-a6cff75(zod@3.23.8) version: 0.2.5-9a1b047(zod@3.23.8)
'@logto/connector-kit': '@logto/connector-kit':
specifier: workspace:^4.0.0 specifier: workspace:^4.0.0
version: link:../toolkit/connector-kit version: link:../toolkit/connector-kit
@ -5080,6 +5080,10 @@ packages:
resolution: {integrity: sha512-19MGifwYGxjQMPrm6monfoQyOp9UTL/chtZE0JugppNwvvLyqr3Nx0maCHuwrydLt0ImBSgVmPW1cJVvu2tVPg==} resolution: {integrity: sha512-19MGifwYGxjQMPrm6monfoQyOp9UTL/chtZE0JugppNwvvLyqr3Nx0maCHuwrydLt0ImBSgVmPW1cJVvu2tVPg==}
engines: {node: ^20.9.0} engines: {node: ^20.9.0}
'@logto/cloud@0.2.5-9a1b047':
resolution: {integrity: sha512-1vOno0Gg5B6f2UOYW275e7zSwItNXJXJdwtcYLn9ThvRi3Lvers+TXDN4SQ/0+l39sjN/1WJ9ZCKvGH89AIGxA==}
engines: {node: ^20.9.0}
'@logto/cloud@0.2.5-a6cff75': '@logto/cloud@0.2.5-a6cff75':
resolution: {integrity: sha512-VlW8MI8RU5dWbHOXY6HjcaC4cqN+I0FIplZQnQjsf00R7K1EFvWfdzNqMcPsiK0ljnyEkRBH4GO77zJ/MYsNdg==} resolution: {integrity: sha512-VlW8MI8RU5dWbHOXY6HjcaC4cqN+I0FIplZQnQjsf00R7K1EFvWfdzNqMcPsiK0ljnyEkRBH4GO77zJ/MYsNdg==}
engines: {node: ^20.9.0} engines: {node: ^20.9.0}
@ -14722,6 +14726,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
'@logto/cloud@0.2.5-9a1b047(zod@3.23.8)':
dependencies:
'@silverhand/essentials': 2.9.1
'@withtyped/server': 0.13.6(zod@3.23.8)
transitivePeerDependencies:
- zod
'@logto/cloud@0.2.5-a6cff75(zod@3.23.8)': '@logto/cloud@0.2.5-a6cff75(zod@3.23.8)':
dependencies: dependencies:
'@silverhand/essentials': 2.9.1 '@silverhand/essentials': 2.9.1