0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(console): fix enterprise tenant current sku always return dev (#6869)

* fix(console): fix enterprise tenant current sku always dev

fix the enterprise tenant current sku always dev bug

* fix(console): fix the lint error

fix the lint error
This commit is contained in:
simeng-li 2024-12-10 18:33:02 +08:00 committed by GitHub
parent ba84b1aed8
commit ad4800fd24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 59 additions and 35 deletions

View file

@ -35,7 +35,7 @@ function SelectTenantPlanModal({ tenantData, onClose }: Props) {
const { subscribe } = useSubscribe(); const { subscribe } = useSubscribe();
const cloudApi = useCloudApi({ hideErrorToast: true }); const cloudApi = useCloudApi({ hideErrorToast: true });
const reservedBasicLogtoSkus = conditional(logtoSkus && pickupFeaturedLogtoSkus(logtoSkus)); const reservedBasicLogtoSkus = conditional(pickupFeaturedLogtoSkus(logtoSkus));
if (!reservedBasicLogtoSkus || !tenantData) { if (!reservedBasicLogtoSkus || !tenantData) {
return null; return null;

View file

@ -1,9 +1,9 @@
import { cond, pick } from '@silverhand/essentials'; import { pick } from '@silverhand/essentials';
import { useContext, useEffect, useMemo } from 'react'; import { useContext, useEffect, useMemo } from 'react';
import useSWR from 'swr'; import useSWR from 'swr';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type NewSubscriptionUsageResponse } from '@/cloud/types/router'; import { type LogtoSkuResponse, type NewSubscriptionUsageResponse } from '@/cloud/types/router';
import { import {
defaultLogtoSku, defaultLogtoSku,
defaultTenantResponse, defaultTenantResponse,
@ -12,7 +12,8 @@ import {
} from '@/consts'; } from '@/consts';
import { isCloud } from '@/consts/env'; import { isCloud } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import useLogtoSkus from '@/hooks/use-logto-skus'; import { LogtoSkuType } from '@/types/skus';
import { formatLogtoSkusResponses } from '@/utils/subscription';
import useSubscription from '../../hooks/use-subscription'; import useSubscription from '../../hooks/use-subscription';
@ -22,7 +23,6 @@ const useNewSubscriptionData: () => NewSubscriptionContext & { isLoading: boolea
const cloudApi = useCloudApi(); const cloudApi = useCloudApi();
const { currentTenant, currentTenantId, updateTenant } = useContext(TenantsContext); const { currentTenant, currentTenantId, updateTenant } = useContext(TenantsContext);
const { isLoading: isLogtoSkusLoading, data: fetchedLogtoSkus } = useLogtoSkus();
const { const {
data: currentSubscription, data: currentSubscription,
@ -42,7 +42,21 @@ const useNewSubscriptionData: () => NewSubscriptionContext & { isLoading: boolea
}) })
); );
const logtoSkus = useMemo(() => cond(isCloud && fetchedLogtoSkus) ?? [], [fetchedLogtoSkus]); // Fetch tenant specific available SKUs
// Unlike the `useLogtoSkus` hook, apart from public available SKUs, this hook also fetches tenant specific private SKUs
// For enterprise tenants who have their own private SKUs, and all grandfathered plan tenants,
// this is the only place to retrieve their current SKU data.
const { isLoading: isLogtoSkusLoading, data: fetchedLogtoSkus } = useSWR<
LogtoSkuResponse[],
Error
>(isCloud && currentTenantId && `/api/tenants/${currentTenantId}/available-skus`, async () =>
cloudApi.get('/api/tenants/:tenantId/available-skus', {
params: { tenantId: currentTenantId },
search: { type: LogtoSkuType.Basic },
})
);
const logtoSkus = useMemo(() => formatLogtoSkusResponses(fetchedLogtoSkus), [fetchedLogtoSkus]);
const currentSku = useMemo( const currentSku = useMemo(
() => logtoSkus.find((logtoSku) => logtoSku.id === currentTenant?.planId) ?? defaultLogtoSku, () => logtoSkus.find((logtoSku) => logtoSku.id === currentTenant?.planId) ?? defaultLogtoSku,

View file

@ -5,17 +5,22 @@ import useSWRImmutable from 'swr/immutable';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api'; import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type LogtoSkuResponse } from '@/cloud/types/router'; import { type LogtoSkuResponse } from '@/cloud/types/router';
import { isCloud } from '@/consts/env'; import { isCloud } from '@/consts/env';
import { featuredPlanIdOrder } from '@/consts/subscriptions';
// Used in the docs // Used in the docs
// eslint-disable-next-line unused-imports/no-unused-imports // eslint-disable-next-line unused-imports/no-unused-imports
import TenantAccess from '@/containers/TenantAccess'; import TenantAccess from '@/containers/TenantAccess';
// eslint-disable-next-line unused-imports/no-unused-imports -- for jsDoc use
import type { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { LogtoSkuType } from '@/types/skus'; import { LogtoSkuType } from '@/types/skus';
import { sortBy } from '@/utils/sort'; import { formatLogtoSkusResponses } from '@/utils/subscription';
import { addSupportQuota } from '@/utils/subscription';
/** /**
* Fetch Logto SKUs from the cloud API. * Fetch public Logto SKUs from the cloud API.
* Note: If you want to retrieve Logto SKUs under the {@link TenantAccess} component, use `SubscriptionDataContext` instead. *
* @remarks
* Note: This hook is used for retrieving public available Logto SKUs for all the users.
* If you want to retrieve tenant specific available Logto SKUs under the {@link TenantAccess} component,
* e.g. For enterprise tenant who have their own private SKUs, and all grandfathered plan tenants,
* use the logtoSkus from the {@link SubscriptionDataContext} instead.
*/ */
const useLogtoSkus = () => { const useLogtoSkus = () => {
const cloudApi = useCloudApi(); const cloudApi = useCloudApi();
@ -30,18 +35,10 @@ const useLogtoSkus = () => {
const { data: logtoSkuResponse } = useSwrResponse; const { data: logtoSkuResponse } = useSwrResponse;
const logtoSkus: Optional<LogtoSkuResponse[]> = useMemo(() => { const logtoSkus: Optional<LogtoSkuResponse[]> = useMemo(
if (!logtoSkuResponse) { () => formatLogtoSkusResponses(logtoSkuResponse),
return; [logtoSkuResponse]
} );
return logtoSkuResponse
.map((logtoSku) => addSupportQuota(logtoSku))
.slice()
.sort(({ id: previousId }, { id: nextId }) =>
sortBy(featuredPlanIdOrder)(previousId, nextId)
);
}, [logtoSkuResponse]);
return { return {
...useSwrResponse, ...useSwrResponse,

View file

@ -14,7 +14,6 @@ import SkuName from '@/components/SkuName';
import { checkoutStateQueryKey } from '@/consts/subscriptions'; import { checkoutStateQueryKey } from '@/consts/subscriptions';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider'; import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import useLogtoSkus from '@/hooks/use-logto-skus';
import useTenantPathname from '@/hooks/use-tenant-pathname'; import useTenantPathname from '@/hooks/use-tenant-pathname';
import { clearLocalCheckoutSession, getLocalCheckoutSession } from '@/utils/checkout'; import { clearLocalCheckoutSession, getLocalCheckoutSession } from '@/utils/checkout';
@ -32,9 +31,6 @@ function CheckoutSuccessCallback() {
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey); const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);
const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {}; const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {};
const { data: logtoSkus, error: fetchLogtoSkusError } = useLogtoSkus();
const isLoadingLogtoSkus = !logtoSkus && !fetchLogtoSkusError;
// Note: if we can't get the subscription results in 10 seconds, we will redirect to the console home page // Note: if we can't get the subscription results in 10 seconds, we will redirect to the console home page
useTimer({ useTimer({
autoStart: true, autoStart: true,
@ -63,7 +59,6 @@ function CheckoutSuccessCallback() {
); );
const checkoutTenantId = stripeCheckoutSession?.tenantId; const checkoutTenantId = stripeCheckoutSession?.tenantId;
const checkoutPlanId = stripeCheckoutSession?.planId;
const checkoutSkuId = stripeCheckoutSession?.skuId; const checkoutSkuId = stripeCheckoutSession?.skuId;
const { data: tenantSubscription } = useSWR( const { data: tenantSubscription } = useSWR(
@ -80,19 +75,18 @@ function CheckoutSuccessCallback() {
const isCheckoutSuccessful = const isCheckoutSuccessful =
checkoutTenantId && checkoutTenantId &&
stripeCheckoutSession.status === 'complete' && stripeCheckoutSession.status === 'complete' &&
!isLoadingLogtoSkus &&
checkoutSkuId === tenantSubscription?.planId; checkoutSkuId === tenantSubscription?.planId;
useEffect(() => { useEffect(() => {
if (isCheckoutSuccessful) { if (isCheckoutSuccessful) {
clearLocalCheckoutSession(); clearLocalCheckoutSession();
const checkoutSku = logtoSkus?.find((sku) => sku.id === checkoutPlanId); // Make the typescript happy checkoutSkuId should not be empty here
if (checkoutSku) { if (checkoutSkuId) {
toast.success( toast.success(
<Trans <Trans
components={{ components={{
name: <SkuName skuId={checkoutSku.id} />, name: <SkuName skuId={checkoutSkuId} />,
}} }}
> >
{t(isDowngrade ? 'downgrade_success' : 'upgrade_success')} {t(isDowngrade ? 'downgrade_success' : 'upgrade_success')}
@ -124,12 +118,11 @@ function CheckoutSuccessCallback() {
} }
}, [ }, [
callbackPage, callbackPage,
checkoutPlanId, checkoutSkuId,
checkoutTenantId, checkoutTenantId,
currentTenantId, currentTenantId,
isCheckoutSuccessful, isCheckoutSuccessful,
isDowngrade, isDowngrade,
logtoSkus,
navigate, navigate,
navigateTenant, navigateTenant,
onCurrentSubscriptionUpdated, onCurrentSubscriptionUpdated,

View file

@ -9,7 +9,7 @@ import { ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
import { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions'; import { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions';
import { type LogtoSkuQuota } from '@/types/skus'; import { type LogtoSkuQuota } from '@/types/skus';
export const addSupportQuota = (logtoSkuResponse: LogtoSkuResponse) => { const addSupportQuota = (logtoSkuResponse: LogtoSkuResponse) => {
const { id, quota } = logtoSkuResponse; const { id, quota } = logtoSkuResponse;
return { return {
@ -24,6 +24,26 @@ export const addSupportQuota = (logtoSkuResponse: LogtoSkuResponse) => {
}; };
}; };
/**
* Format Logto SKUs responses.
*
* - add support quota to the SKUs.
* - Sort the SKUs by the order of `featuredPlanIdOrder`.
*/
export const formatLogtoSkusResponses = (logtoSkus: LogtoSkuResponse[] | undefined) => {
if (!logtoSkus) {
return [];
}
return logtoSkus
.map((logtoSku) => addSupportQuota(logtoSku))
.slice()
.sort(
({ id: previousId }, { id: nextId }) =>
featuredPlanIdOrder.indexOf(previousId) - featuredPlanIdOrder.indexOf(nextId)
);
};
const getSubscriptionPlanOrderById = (id: string) => { const getSubscriptionPlanOrderById = (id: string) => {
const index = featuredPlanIdOrder.indexOf(id); const index = featuredPlanIdOrder.indexOf(id);