Skip to content

Commit

Permalink
fix: fix console issues for add-on (#6443)
Browse files Browse the repository at this point in the history
* fix: fix console issues for add-on

* refactor: refactor code

* refactor: update

* fix: fix method use case
  • Loading branch information
darcyYe authored Aug 15, 2024
1 parent bb98ea8 commit d2220f1
Show file tree
Hide file tree
Showing 29 changed files with 133 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
if (
selectedType === ApplicationType.MachineToMachine &&
isDevFeaturesEnabled &&
hasMachineToMachineAppsReachedLimit &&
planId === ReservedPlanId.Pro
) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function CreateForm({
}: Props) {
const {
handleSubmit,
watch,
control,
register,
formState: { errors, isSubmitting },
Expand Down Expand Up @@ -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={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -38,7 +47,9 @@ function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, classN
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
}}
>
{t(tooltipKey)}
{t(tooltipKey, {
price: unitPrice,
})}
</Trans>
}
>
Expand Down
3 changes: 2 additions & 1 deletion packages/console/src/components/PlanUsage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 }
Expand Down
24 changes: 23 additions & 1 deletion packages/console/src/components/PlanUsage/utils.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,6 +25,7 @@ type UsageKey = Pick<
| 'hooksLimit'
>;

// We decide not to show `hooksLimit` usage in console for now.
export const usageKeys: Array<keyof UsageKey> = [
'mauLimit',
'organizationsEnabled',
Expand All @@ -24,9 +35,20 @@ export const usageKeys: Array<keyof UsageKey> = [
'machineToMachineLimit',
'tenantMembersLimit',
'tokenLimit',
'hooksLimit',
];

export const usageKeyPriceMap: Record<keyof UsageKey, number> = {
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'>
Expand Down
2 changes: 2 additions & 0 deletions packages/console/src/consts/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) === */

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/console/src/hooks/use-subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 7 additions & 8 deletions packages/console/src/onboarding/pages/CreateTenant/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
);
}

if (isDevFeaturesEnabled && planId === ReservedPlanId.Pro) {
if (isDevFeaturesEnabled && hasReachedLimit && planId === ReservedPlanId.Pro) {
return (
<AddOnNoticeFooter
isLoading={isCreationLoading}
Expand Down
6 changes: 6 additions & 0 deletions packages/console/src/pages/CheckoutSuccessCallback/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GtagConversionId, reportToGoogle } from '@/components/Conversion/utils'
import PlanName from '@/components/PlanName';
import { isDevFeaturesEnabled } from '@/consts/env';
import { checkoutStateQueryKey } from '@/consts/subscriptions';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import useLogtoSkus from '@/hooks/use-logto-skus';
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
Expand All @@ -28,6 +29,7 @@ function CheckoutSuccessCallback() {
const { navigate } = useTenantPathname();
const cloudApi = useCloudApi({ hideErrorToast: true });
const { currentTenantId, navigateTenant } = useContext(TenantsContext);
const { onCurrentSubscriptionUpdated } = useContext(SubscriptionDataContext);
const { search } = useLocation();
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);
const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {};
Expand Down Expand Up @@ -121,6 +123,8 @@ function CheckoutSuccessCallback() {
}
}

onCurrentSubscriptionUpdated(tenantSubscription);

// No need to check `isDowngrade` here, since a downgrade must occur in a tenant with a Pro
// plan, and the purchase conversion has already been reported using the same tenant ID. We
// use the tenant ID as the transaction ID, so there's no concern about duplicate conversion
Expand All @@ -147,8 +151,10 @@ function CheckoutSuccessCallback() {
logtoSkus,
navigate,
navigateTenant,
onCurrentSubscriptionUpdated,
subscriptionPlans,
t,
tenantSubscription,
]);

if (!isValidSession && !isLoadingPlans) {
Expand Down
4 changes: 3 additions & 1 deletion packages/console/src/pages/Mfa/MfaForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ type Props = {
};

function MfaForm({ data, onMfaUpdated }: Props) {
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
const { currentPlan, currentSubscriptionQuota, mutateSubscriptionQuotaAndUsages } =
useContext(SubscriptionDataContext);
const isMfaDisabled =
isCloud &&
!(isDevFeaturesEnabled ? currentSubscriptionQuota.mfaEnabled : currentPlan.quota.mfaEnabled);
Expand Down Expand Up @@ -77,6 +78,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
json: { mfa: mfaConfig },
})
.json<SignInExperience>();
mutateSubscriptionQuotaAndUsages();
reset(convertMfaConfigToForm(updatedMfaConfig));
toast.success(t('general.saved'));
onMfaUpdated(updatedMfaConfig);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -39,6 +39,12 @@ function Subscription() {
})
);

useEffect(() => {
if (isCloud && isDevFeaturesEnabled) {
onCurrentSubscriptionUpdated();
}
}, [onCurrentSubscriptionUpdated]);

if (isLoading) {
return <Skeleton />;
}
Expand Down
Loading

0 comments on commit d2220f1

Please sign in to comment.