Skip to content

Commit

Permalink
refactor: add periodic usage fallback to avoid breaking changes
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyYe committed Aug 15, 2024
1 parent c355ab9 commit 0e1d4f1
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 27 deletions.
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-a1a4d0e",
"@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
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
24 changes: 21 additions & 3 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, 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,20 +21,37 @@ type Props = {
readonly currentSubscription: Subscription;
/** @deprecated */
readonly currentPlan: SubscriptionPlan;
readonly periodicUsage: NewSubscriptionPeriodicUsage;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

function PlanUsage({ currentSubscription, currentPlan, periodicUsage }: 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] = [
periodicUsage.mauLimit,
isDevFeaturesEnabled ? currentSubscriptionQuota.mauLimit : currentPlan.quota.mauLimit,
Expand Down
2 changes: 2 additions & 0 deletions packages/console/src/consts/tenants.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ReservedPlanId } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo, useState } from 'react';

import { toastResponseError } from '@/cloud/hooks/use-cloud-api';
Expand All @@ -24,15 +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;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

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

const [isLoading, setIsLoading] = useState(false);
const proPlan = useMemo(
Expand All @@ -45,6 +51,22 @@ function MauLimitExceededNotification({ currentPlan, periodicUsage, className }:
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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cond } from '@silverhand/essentials';
import { cond, conditional } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';

import { type Subscription, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
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,10 +24,11 @@ type Props = {
readonly subscription: Subscription;
/** @deprecated */
readonly subscriptionPlan: SubscriptionPlan;
readonly periodicUsage: NewSubscriptionPeriodicUsage;
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
};

function CurrentPlan({ subscription, subscriptionPlan, periodicUsage }: Props) {
function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodicUsage }: Props) {
const { currentTenant } = useContext(TenantsContext);
const { currentSku, currentSubscription, currentSubscriptionQuota } =
useContext(SubscriptionDataContext);
const {
Expand All @@ -35,6 +37,18 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage }: Props) {
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 @@ -43,6 +57,10 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage }: Props) {
[currentSku.unitPrice, currentSubscription.upcomingInvoice?.subtotal]
);

if (!periodicUsage) {
return null;
}

const hasTokenSurpassedLimit = isDevFeaturesEnabled
? hasSurpassedSubscriptionQuotaLimit({
quotaKey: 'tokenLimit',
Expand All @@ -69,15 +87,15 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage }: Props) {
<PlanUsage
currentSubscription={subscription}
currentPlan={subscriptionPlan}
periodicUsage={periodicUsage}
periodicUsage={rawPeriodicUsage}
/>
</FormField>
<FormField title="subscription.next_bill">
<BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} />
</FormField>
<MauLimitExceedNotification
currentPlan={subscriptionPlan}
periodicUsage={periodicUsage}
periodicUsage={rawPeriodicUsage}
className={styles.notification}
/>
<ChargeNotification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function Subscription() {
const reservedPlans = pickupFeaturedPlans(subscriptionPlans);
const reservedSkus = pickupFeaturedLogtoSkus(logtoSkus);

const { data: periodicUsage } = useSWR(
const { data: periodicUsage, isLoading } = useSWR(
isCloud &&
isDevFeaturesEnabled &&
`/api/tenants/${currentTenantId}/subscription/periodic-usage`,
Expand All @@ -39,7 +39,7 @@ function Subscription() {
})
);

if (!periodicUsage) {
if (isLoading) {
return <Skeleton />;
}

Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0e1d4f1

Please sign in to comment.