diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx
index 1f5845f4a6b..63e7975900a 100644
--- a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx
+++ b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx
@@ -37,6 +37,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
if (
selectedType === ApplicationType.MachineToMachine &&
isDevFeaturesEnabled &&
+ hasMachineToMachineAppsReachedLimit &&
planId === ReservedPlanId.Pro
) {
return (
diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx
index 097c7cf7010..b3fcb617869 100644
--- a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx
+++ b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx
@@ -49,6 +49,7 @@ function CreateForm({
}: Props) {
const {
handleSubmit,
+ watch,
control,
register,
formState: { errors, isSubmitting },
@@ -122,7 +123,10 @@ function CreateForm({
title="applications.create"
subtitle={subtitleElement}
paywall={conditional(
- isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
+ isDevFeaturesEnabled &&
+ watch('type') === ApplicationType.MachineToMachine &&
+ planId === ReservedPlanId.Pro &&
+ ReservedPlanId.Pro
)}
size={defaultCreateType ? 'medium' : 'large'}
footer={
diff --git a/packages/console/src/components/PlanUsage/ProPlanUsageCard/index.tsx b/packages/console/src/components/PlanUsage/ProPlanUsageCard/index.tsx
index 70b60b8876e..31403dded65 100644
--- a/packages/console/src/components/PlanUsage/ProPlanUsageCard/index.tsx
+++ b/packages/console/src/components/PlanUsage/ProPlanUsageCard/index.tsx
@@ -19,10 +19,19 @@ export type Props = {
readonly usageKey: AdminConsoleKey;
readonly titleKey: AdminConsoleKey;
readonly tooltipKey: AdminConsoleKey;
+ readonly unitPrice: number;
readonly className?: string;
};
-function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, className }: Props) {
+function ProPlanUsageCard({
+ usage,
+ quota,
+ unitPrice,
+ usageKey,
+ titleKey,
+ tooltipKey,
+ className,
+}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
@@ -38,7 +47,9 @@ function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, classN
a: ,
}}
>
- {t(tooltipKey)}
+ {t(tooltipKey, {
+ price: unitPrice,
+ })}
}
>
diff --git a/packages/console/src/components/PlanUsage/index.tsx b/packages/console/src/components/PlanUsage/index.tsx
index c48e06c0274..640bd96a0b3 100644
--- a/packages/console/src/components/PlanUsage/index.tsx
+++ b/packages/console/src/components/PlanUsage/index.tsx
@@ -14,7 +14,7 @@ import { formatPeriod } from '@/utils/subscription';
import ProPlanUsageCard, { type Props as ProPlanUsageCardProps } from './ProPlanUsageCard';
import styles from './index.module.scss';
-import { usageKeys, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
+import { usageKeys, usageKeyPriceMap, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
type Props = {
/** @deprecated */
@@ -67,6 +67,7 @@ function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodi
usageKey: `subscription.usage.${usageKeyMap[key]}`,
titleKey: `subscription.usage.${titleKeyMap[key]}`,
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
+ unitPrice: usageKeyPriceMap[key],
...cond(
key === 'tokenLimit' &&
currentSubscriptionQuota.tokenLimit && { quota: currentSubscriptionQuota.tokenLimit }
diff --git a/packages/console/src/components/PlanUsage/utils.ts b/packages/console/src/components/PlanUsage/utils.ts
index 4234148079c..39edb7ae25c 100644
--- a/packages/console/src/components/PlanUsage/utils.ts
+++ b/packages/console/src/components/PlanUsage/utils.ts
@@ -1,6 +1,16 @@
import { type TFuncKey } from 'i18next';
import { type NewSubscriptionQuota } from '@/cloud/types/router';
+import {
+ resourceAddOnUnitPrice,
+ machineToMachineAddOnUnitPrice,
+ tenantMembersAddOnUnitPrice,
+ mfaAddOnUnitPrice,
+ enterpriseSsoAddOnUnitPrice,
+ organizationAddOnUnitPrice,
+ tokenAddOnUnitPrice,
+ hooksAddOnUnitPrice,
+} from '@/consts/subscriptions';
type UsageKey = Pick<
NewSubscriptionQuota,
@@ -15,6 +25,7 @@ type UsageKey = Pick<
| 'hooksLimit'
>;
+// We decide not to show `hooksLimit` usage in console for now.
export const usageKeys: Array = [
'mauLimit',
'organizationsEnabled',
@@ -24,9 +35,20 @@ export const usageKeys: Array = [
'machineToMachineLimit',
'tenantMembersLimit',
'tokenLimit',
- 'hooksLimit',
];
+export const usageKeyPriceMap: Record = {
+ mauLimit: 0,
+ organizationsEnabled: organizationAddOnUnitPrice,
+ mfaEnabled: mfaAddOnUnitPrice,
+ enterpriseSsoLimit: enterpriseSsoAddOnUnitPrice,
+ resourcesLimit: resourceAddOnUnitPrice,
+ machineToMachineLimit: machineToMachineAddOnUnitPrice,
+ tenantMembersLimit: tenantMembersAddOnUnitPrice,
+ tokenLimit: tokenAddOnUnitPrice,
+ hooksLimit: hooksAddOnUnitPrice,
+};
+
export const usageKeyMap: Record<
keyof UsageKey,
TFuncKey<'translation', 'admin_console.subscription.usage'>
diff --git a/packages/console/src/consts/subscriptions.ts b/packages/console/src/consts/subscriptions.ts
index b61278333b8..5062e336305 100644
--- a/packages/console/src/consts/subscriptions.ts
+++ b/packages/console/src/consts/subscriptions.ts
@@ -18,6 +18,8 @@ export const tenantMembersAddOnUnitPrice = 8;
export const mfaAddOnUnitPrice = 48;
export const enterpriseSsoAddOnUnitPrice = 48;
export const organizationAddOnUnitPrice = 48;
+export const tokenAddOnUnitPrice = 80;
+export const hooksAddOnUnitPrice = 2;
/* === Add-on unit price (in USD) === */
/**
diff --git a/packages/console/src/hooks/use-subscribe.ts b/packages/console/src/hooks/use-subscribe.ts
index b2d611d69e0..50bf3df7628 100644
--- a/packages/console/src/hooks/use-subscribe.ts
+++ b/packages/console/src/hooks/use-subscribe.ts
@@ -103,14 +103,15 @@ const useSubscribe = () => {
// Should not use hard-coded plan update here, need to update the tenant's subscription data with response from corresponding API.
if (isDevFeaturesEnabled) {
- const { id, ...rest } = await cloudApi.get('/api/tenants/:tenantId/subscription', {
+ const subscription = await cloudApi.get('/api/tenants/:tenantId/subscription', {
params: {
tenantId,
},
});
mutateSubscriptionQuotaAndUsages();
- onCurrentSubscriptionUpdated();
+ onCurrentSubscriptionUpdated(subscription);
+ const { id, ...rest } = subscription;
updateTenant(tenantId, {
planId: rest.planId,
diff --git a/packages/console/src/onboarding/pages/CreateTenant/index.tsx b/packages/console/src/onboarding/pages/CreateTenant/index.tsx
index 02b4a3584c8..9d591ccaba3 100644
--- a/packages/console/src/onboarding/pages/CreateTenant/index.tsx
+++ b/packages/console/src/onboarding/pages/CreateTenant/index.tsx
@@ -81,14 +81,13 @@ function CreateTenant() {
if (collaboratorEmails.length > 0) {
// Should not block the onboarding flow if the invitation fails.
try {
- await Promise.all(
- collaboratorEmails.map(async (email) =>
- tenantCloudApi.post('/api/tenants/:tenantId/invitations', {
- params: { tenantId: newTenant.id },
- body: { invitee: email.value, roleName: TenantRole.Collaborator },
- })
- )
- );
+ await tenantCloudApi.post('/api/tenants/:tenantId/invitations', {
+ params: { tenantId: newTenant.id },
+ body: {
+ invitee: collaboratorEmails.map(({ value }) => value),
+ roleName: TenantRole.Collaborator,
+ },
+ });
toast.success(t('tenant_members.messages.invitation_sent'));
} catch {
toast.error(t('tenants.create_modal.invitation_failed', { duration: 5 }));
diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx
index 7c8b0e787f8..3dc2ddba28a 100644
--- a/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx
+++ b/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx
@@ -53,7 +53,7 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
);
}
- if (isDevFeaturesEnabled && planId === ReservedPlanId.Pro) {
+ if (isDevFeaturesEnabled && hasReachedLimit && planId === ReservedPlanId.Pro) {
return (
();
+ mutateSubscriptionQuotaAndUsages();
reset(convertMfaConfigToForm(updatedMfaConfig));
toast.success(t('general.saved'));
onMfaUpdated(updatedMfaConfig);
diff --git a/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx
index 81b48c30a5b..aa590afaf1e 100644
--- a/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx
+++ b/packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx
@@ -1,4 +1,4 @@
-import { cond, conditional } from '@silverhand/essentials';
+import { cond } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';
import { type Subscription, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
@@ -28,9 +28,9 @@ type Props = {
};
function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodicUsage }: Props) {
- const { currentTenant } = useContext(TenantsContext);
const { currentSku, currentSubscription, currentSubscriptionQuota } =
useContext(SubscriptionDataContext);
+ const { currentTenant } = useContext(TenantsContext);
const {
id,
name,
@@ -40,7 +40,7 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodi
const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
- conditional(
+ cond(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
@@ -50,11 +50,12 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodi
);
/**
- * 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. `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?.subtotal]
+ [currentSku.unitPrice, currentSubscription.upcomingInvoice]
);
if (!periodicUsage) {
diff --git a/packages/console/src/pages/TenantSettings/Subscription/index.tsx b/packages/console/src/pages/TenantSettings/Subscription/index.tsx
index 4d90ba9a62f..21f8acab2b6 100644
--- a/packages/console/src/pages/TenantSettings/Subscription/index.tsx
+++ b/packages/console/src/pages/TenantSettings/Subscription/index.tsx
@@ -1,4 +1,4 @@
-import { useContext } from 'react';
+import { useContext, useEffect } from 'react';
import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
@@ -39,6 +39,12 @@ function Subscription() {
})
);
+ useEffect(() => {
+ if (isCloud && isDevFeaturesEnabled) {
+ onCurrentSubscriptionUpdated();
+ }
+ }, [onCurrentSubscriptionUpdated]);
+
if (isLoading) {
return ;
}
diff --git a/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx b/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx
index 07c69f1c6ca..3438db34ebe 100644
--- a/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx
+++ b/packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx
@@ -18,6 +18,7 @@ import Select, { type Option } from '@/ds-components/Select';
import TextLink from '@/ds-components/TextLink';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import modalStyles from '@/scss/modal.module.scss';
+import { hasReachedSubscriptionQuotaLimit } from '@/utils/quota';
import InviteEmailsInput from '../InviteEmailsInput';
import useEmailInputUtils from '../InviteEmailsInput/hooks';
@@ -41,6 +42,8 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
const { show } = useConfirmModal();
const {
currentSubscription: { planId },
+ currentSubscriptionQuota,
+ currentSubscriptionUsage: { tenantMembersLimit },
} = useContext(SubscriptionDataContext);
const formMethods = useForm({
@@ -72,6 +75,12 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
[t]
);
+ const hasTenantMembersReachedLimit = hasReachedSubscriptionQuotaLimit({
+ quotaKey: 'tenantMembersLimit',
+ usage: tenantMembersLimit,
+ quota: currentSubscriptionQuota,
+ });
+
const onSubmit = handleSubmit(async ({ emails, role }) => {
if (role === TenantRole.Admin) {
const [result] = await show({
@@ -89,19 +98,17 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
}
setIsLoading(true);
- try {
- await Promise.all(
- emails.map(async (email) =>
- cloudApi.post('/api/tenants/:tenantId/invitations', {
- params: { tenantId: currentTenantId },
- body: { invitee: email.value, roleName: role },
- })
- )
- );
- toast.success(t('tenant_members.messages.invitation_sent'));
- onClose(true);
- } finally {
- setIsLoading(false);
+ if (emails.length > 0) {
+ try {
+ await cloudApi.post('/api/tenants/:tenantId/invitations', {
+ params: { tenantId: currentTenantId },
+ body: { invitee: emails.map(({ value }) => value), roleName: role },
+ });
+ toast.success(t('tenant_members.messages.invitation_sent'));
+ onClose(true);
+ } finally {
+ setIsLoading(false);
+ }
}
});
@@ -123,24 +130,26 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
subtitle="tenant_members.invite_modal.subtitle"
footer={
conditional(
- isDevFeaturesEnabled && planId === ReservedPlanId.Pro && (
-
- ,
- a: ,
- }}
+ isDevFeaturesEnabled &&
+ hasTenantMembersReachedLimit &&
+ planId === ReservedPlanId.Pro && (
+
- {t('upsell.add_on.footer.tenant_members', {
- price: tenantMembersAddOnUnitPrice,
- })}
-
-
- )
+ ,
+ a: ,
+ }}
+ >
+ {t('upsell.add_on.footer.tenant_members', {
+ price: tenantMembersAddOnUnitPrice,
+ })}
+
+
+ )
) ?? (