Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(console): separate subscription based usage #6448

Merged
merged 3 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"devDependencies": {
"@fontsource/roboto-mono": "^5.0.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/core-kit": "workspace:^2.5.0",
"@logto/elements": "workspace:^0.0.0",
Expand Down
6 changes: 5 additions & 1 deletion packages/console/src/cloud/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ export type NewSubscriptionUsageResponse = GuardedResponse<
/** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */
export type NewSubscriptionQuota = NewSubscriptionUsageResponse['quota'];
/** 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 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 ===== */

export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;
Expand Down
17 changes: 8 additions & 9 deletions packages/console/src/components/MauExceededModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { cond } from '@silverhand/essentials';
import { useContext, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
Expand All @@ -6,6 +7,7 @@ import PlanUsage from '@/components/PlanUsage';
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 FormField from '@/ds-components/FormField';
import InlineNotification from '@/ds-components/InlineNotification';
Expand All @@ -18,13 +20,8 @@ import PlanName from '../PlanName';
import styles from './index.module.scss';

function MauExceededModal() {
const {
currentPlan,
currentSubscription,
currentSku,
currentSubscriptionQuota,
currentSubscriptionUsage,
} = useContext(SubscriptionDataContext);
const { currentPlan, currentSubscription, currentSku } = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);

const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { navigate } = useTenantPathname();
Expand All @@ -41,8 +38,10 @@ function MauExceededModal() {
const { name: planName } = currentPlan;

const isMauExceeded =
currentSubscriptionQuota.mauLimit !== null &&
currentSubscriptionUsage.mauLimit >= currentSubscriptionQuota.mauLimit;
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain, prettier/prettier
cond(currentTenant && currentTenant.quota.mauLimit !== null &&
currentTenant.usage.activeUsers >= currentTenant.quota.mauLimit
);

if (!isMauExceeded) {
return null;
Expand Down
32 changes: 27 additions & 5 deletions packages/console/src/components/PlanUsage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { ReservedPlanId } from '@logto/schemas';
import { cond, conditional } from '@silverhand/essentials';
import classNames from 'classnames';
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 { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { formatPeriod } from '@/utils/subscription';
Expand All @@ -20,28 +21,49 @@ type Props = {
readonly currentSubscription: Subscription;
/** @deprecated */
readonly currentPlan: SubscriptionPlan;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

function PlanUsage({ currentSubscription, currentPlan }: Props) {
function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodicUsage }: Props) {
const {
currentSubscriptionQuota,
currentSubscriptionUsage,
currentSubscription: currentSubscriptionFromNewPricingModel,
} = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);

const { currentPeriodStart, currentPeriodEnd } = isDevFeaturesEnabled
? currentSubscriptionFromNewPricingModel
: currentSubscription;

const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant, rawPeriodicUsage]
);

if (!periodicUsage) {
return null;
}

const [activeUsers, mauLimit] = [
currentSubscriptionUsage.mauLimit,
periodicUsage.mauLimit,
isDevFeaturesEnabled ? currentSubscriptionQuota.mauLimit : currentPlan.quota.mauLimit,
];

const usagePercent = conditional(mauLimit && activeUsers / mauLimit);

const usages: ProPlanUsageCardProps[] = usageKeys.map((key) => ({
usage: currentSubscriptionUsage[key],
usage:
key === 'mauLimit' || key === 'tokenLimit'
? periodicUsage[key]
: currentSubscriptionUsage[key],
usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`,
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
Expand Down
6 changes: 4 additions & 2 deletions packages/console/src/consts/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type NewSubscriptionQuota,
type LogtoSkuResponse,
type TenantResponse,
type NewSubscriptionUsage,
type NewSubscriptionCountBasedUsage,
} from '@/cloud/types/router';
import { RegionName } from '@/components/Region';
import { LogtoSkuType } from '@/types/skus';
Expand Down Expand Up @@ -34,9 +34,11 @@ export const defaultTenantResponse: TenantResponse = {
},
usage: {
activeUsers: 0,
tokenUsage: 0,
},
quota: {
mauLimit: null,
tokenLimit: null,
},
openInvoices: [],
isSuspended: false,
Expand Down Expand Up @@ -143,7 +145,7 @@ export const defaultSubscriptionQuota: NewSubscriptionQuota = {
bringYourUiEnabled: false,
};

export const defaultSubscriptionUsage: NewSubscriptionUsage = {
export const defaultSubscriptionUsage: NewSubscriptionCountBasedUsage = {
mauLimit: 0,
tokenLimit: 0,
applicationsLimit: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
type LogtoSkuResponse,
type Subscription,
type NewSubscriptionQuota,
type NewSubscriptionUsage,
type NewSubscriptionCountBasedUsage,
type NewSubscriptionResourceScopeUsage,
type NewSubscriptionRoleScopeUsage,
} from '@/cloud/types/router';
Expand All @@ -21,7 +21,7 @@ type NewSubscriptionSupplementContext = {
logtoSkus: LogtoSkuResponse[];
currentSku: LogtoSkuResponse;
currentSubscriptionQuota: NewSubscriptionQuota;
currentSubscriptionUsage: NewSubscriptionUsage;
currentSubscriptionUsage: NewSubscriptionCountBasedUsage;
currentSubscriptionResourceScopeUsage: NewSubscriptionResourceScopeUsage;
currentSubscriptionRoleScopeUsage: NewSubscriptionRoleScopeUsage;
mutateSubscriptionQuotaAndUsages: () => void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ReservedPlanId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo, useState } from 'react';

import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
import { type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
import { isDevFeaturesEnabled } from '@/consts/env';
import { subscriptionPage } from '@/consts/pages';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
Expand All @@ -23,14 +25,20 @@ type Props = {
/** @deprecated No need to pass in this argument in new pricing model */
readonly currentPlan: SubscriptionPlan;
readonly className?: string;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

function MauLimitExceededNotification({ currentPlan, className }: Props) {
function MauLimitExceededNotification({
currentPlan,
periodicUsage: rawPeriodicUsage,
className,
}: Props) {
const { currentTenantId } = useContext(TenantsContext);
const { subscribe } = useSubscribe();
const { show } = useConfirmModal();
const { subscriptionPlans, logtoSkus, currentSubscriptionQuota, currentSubscriptionUsage } =
const { subscriptionPlans, logtoSkus, currentSubscriptionQuota } =
useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);

const [isLoading, setIsLoading] = useState(false);
const proPlan = useMemo(
Expand All @@ -43,14 +51,30 @@ function MauLimitExceededNotification({ currentPlan, className }: Props) {
quota: { mauLimit: oldPricingModelMauLimit },
} = 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.
const mauLimit = isDevFeaturesEnabled
? currentSubscriptionQuota.mauLimit
: oldPricingModelMauLimit;

if (
mauLimit === null || // Unlimited
currentSubscriptionUsage.mauLimit < mauLimit ||
periodicUsage.mauLimit < mauLimit ||
!proPlan ||
!proSku
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { cond } from '@silverhand/essentials';
import { cond, conditional } from '@silverhand/essentials';
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 ChargeNotification from '@/components/ChargeNotification';
import FormCard from '@/components/FormCard';
Expand All @@ -10,6 +10,7 @@ import PlanName from '@/components/PlanName';
import PlanUsage from '@/components/PlanUsage';
import { isDevFeaturesEnabled } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import FormField from '@/ds-components/FormField';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { hasSurpassedQuotaLimit, hasSurpassedSubscriptionQuotaLimit } from '@/utils/quota';
Expand All @@ -23,17 +24,31 @@ type Props = {
readonly subscription: Subscription;
/** @deprecated */
readonly subscriptionPlan: SubscriptionPlan;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

function CurrentPlan({ subscription, subscriptionPlan }: Props) {
const { currentSku, currentSubscription, currentSubscriptionUsage, currentSubscriptionQuota } =
function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodicUsage }: Props) {
const { currentTenant } = useContext(TenantsContext);
const { currentSku, currentSubscription, currentSubscriptionQuota } =
useContext(SubscriptionDataContext);
const {
id,
name,
quota: { tokenLimit },
} = 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.
*/
Expand All @@ -42,15 +57,19 @@ function CurrentPlan({ subscription, subscriptionPlan }: Props) {
[currentSku.unitPrice, currentSubscription.upcomingInvoice?.subtotal]
);

if (!periodicUsage) {
return null;
}

const hasTokenSurpassedLimit = isDevFeaturesEnabled
? hasSurpassedSubscriptionQuotaLimit({
quotaKey: 'tokenLimit',
usage: currentSubscriptionUsage.tokenLimit,
usage: periodicUsage.tokenLimit,
quota: currentSubscriptionQuota,
})
: hasSurpassedQuotaLimit({
quotaKey: 'tokenLimit',
usage: currentSubscriptionUsage.tokenLimit,
usage: periodicUsage.tokenLimit,
plan: subscriptionPlan,
});

Expand All @@ -65,12 +84,20 @@ function CurrentPlan({ subscription, subscriptionPlan }: Props) {
</div>
</div>
<FormField title="subscription.plan_usage">
<PlanUsage currentSubscription={subscription} currentPlan={subscriptionPlan} />
<PlanUsage
currentSubscription={subscription}
currentPlan={subscriptionPlan}
periodicUsage={rawPeriodicUsage}
/>
</FormField>
<FormField title="subscription.next_bill">
<BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} />
</FormField>
<MauLimitExceedNotification currentPlan={subscriptionPlan} className={styles.notification} />
<MauLimitExceedNotification
currentPlan={subscriptionPlan}
periodicUsage={rawPeriodicUsage}
className={styles.notification}
/>
<ChargeNotification
hasSurpassedLimit={hasTokenSurpassedLimit}
quotaItemPhraseKey="tokens"
Expand Down
Loading
Loading