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

refactor(console): remove subscription plans (#6591)

This commit is contained in:
Darcy Ye 2024-09-25 11:13:39 +08:00 committed by GitHub
parent fa6f8ef498
commit e54baf458a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 10 additions and 163 deletions

View file

@ -7,10 +7,6 @@ type GetTenantAuthRoutes = RouterRoutes<typeof tenantAuthRouter>['get'];
export type GetArrayElementType<T> = T extends Array<infer U> ? U : never; export type GetArrayElementType<T> = T extends Array<infer U> ? U : never;
export type SubscriptionPlanResponse = GuardedResponse<
GetRoutes['/api/subscription-plans']
>[number];
export type LogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/skus']>>; export type LogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/skus']>>;
export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/subscription']>; export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/subscription']>;

View file

@ -9,7 +9,6 @@ import {
} from '@/cloud/types/router'; } from '@/cloud/types/router';
import { RegionName } from '@/components/Region'; import { RegionName } from '@/components/Region';
import { LogtoSkuType } from '@/types/skus'; import { LogtoSkuType } from '@/types/skus';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { adminEndpoint, isCloud } from './env'; import { adminEndpoint, isCloud } from './env';
@ -48,44 +47,6 @@ export const defaultTenantResponse: TenantResponse = {
createdAt: new Date(), createdAt: new Date(),
}; };
/**
* - For cloud, the initial tenant's subscription plan will be fetched from the cloud API.
* - OSS has a fixed subscription plan with `development` id and no cloud API to dynamically fetch the subscription plan.
*/
export const defaultSubscriptionPlan: SubscriptionPlan = {
id: defaultSubscriptionPlanId,
name: 'Development',
createdAt: new Date(),
updatedAt: new Date(),
stripeProducts: [],
quota: {
mauLimit: null,
tokenLimit: null,
applicationsLimit: null,
machineToMachineLimit: null,
resourcesLimit: null,
scopesPerResourceLimit: null,
customDomainEnabled: true,
mfaEnabled: true,
omniSignInEnabled: true,
socialConnectorsLimit: null,
standardConnectorsLimit: null,
rolesLimit: null,
machineToMachineRolesLimit: null,
scopesPerRoleLimit: null,
auditLogsRetentionDays: null,
hooksLimit: null,
organizationsEnabled: true,
ssoEnabled: true,
ticketSupportResponseTime: 48,
thirdPartyApplicationsLimit: null,
tenantMembersLimit: null,
customJwtEnabled: true,
subjectTokenEnabled: true,
bringYourUiEnabled: true,
},
};
/** /**
* - For cloud, the initial tenant's subscription plan will be fetched from the cloud API. * - For cloud, the initial tenant's subscription plan will be fetched from the cloud API.
* - OSS has a fixed subscription plan with `development` id and no cloud API to dynamically fetch the subscription plan. * - OSS has a fixed subscription plan with `development` id and no cloud API to dynamically fetch the subscription plan.

View file

@ -2,7 +2,6 @@ import { noop } from '@silverhand/essentials';
import { createContext, type ReactNode } from 'react'; import { createContext, type ReactNode } from 'react';
import { import {
defaultSubscriptionPlan,
defaultLogtoSku, defaultLogtoSku,
defaultTenantResponse, defaultTenantResponse,
defaultSubscriptionQuota, defaultSubscriptionQuota,
@ -21,8 +20,6 @@ const defaultSubscription = defaultTenantResponse.subscription;
* CAUTION: You should only use this data context under the {@link TenantAccess} component * CAUTION: You should only use this data context under the {@link TenantAccess} component
*/ */
export const SubscriptionDataContext = createContext<FullContext>({ export const SubscriptionDataContext = createContext<FullContext>({
subscriptionPlans: [],
currentPlan: defaultSubscriptionPlan,
currentSubscription: defaultSubscription, currentSubscription: defaultSubscription,
onCurrentSubscriptionUpdated: noop, onCurrentSubscriptionUpdated: noop,
/* ==== For new pricing model ==== */ /* ==== For new pricing model ==== */

View file

@ -6,13 +6,8 @@ import {
type NewSubscriptionResourceScopeUsage, type NewSubscriptionResourceScopeUsage,
type NewSubscriptionRoleScopeUsage, type NewSubscriptionRoleScopeUsage,
} from '@/cloud/types/router'; } from '@/cloud/types/router';
import { type SubscriptionPlan } from '@/types/subscriptions';
export type Context = { export type Context = {
/** @deprecated */
subscriptionPlans: SubscriptionPlan[];
/** @deprecated */
currentPlan: SubscriptionPlan;
currentSubscription: Subscription; currentSubscription: Subscription;
onCurrentSubscriptionUpdated: (subscription?: Subscription) => void; onCurrentSubscriptionUpdated: (subscription?: Subscription) => void;
}; };
@ -28,7 +23,6 @@ type NewSubscriptionSupplementContext = {
mutateSubscriptionQuotaAndUsages: () => void; mutateSubscriptionQuotaAndUsages: () => void;
}; };
export type NewSubscriptionContext = Omit<Context, 'subscriptionPlans' | 'currentPlan'> & export type NewSubscriptionContext = Context & NewSubscriptionSupplementContext;
NewSubscriptionSupplementContext;
export type FullContext = Context & NewSubscriptionSupplementContext; export type FullContext = Context & NewSubscriptionSupplementContext;

View file

@ -1,37 +1,23 @@
import { cond, condString } from '@silverhand/essentials'; import { condString } from '@silverhand/essentials';
import { useContext, useMemo } from 'react'; import { useContext } from 'react';
import { defaultSubscriptionPlan, defaultTenantResponse } from '@/consts'; import { defaultTenantResponse } from '@/consts';
import { isCloud } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider'; import { TenantsContext } from '@/contexts/TenantsProvider';
import useSubscription from '../../hooks/use-subscription'; import useSubscription from '../../hooks/use-subscription';
import useSubscriptionPlans from '../../hooks/use-subscription-plans';
import { type Context } from './types'; import { type Context } from './types';
const useSubscriptionData: () => Context & { isLoading: boolean } = () => { const useSubscriptionData: () => Context & { isLoading: boolean } = () => {
const { currentTenant } = useContext(TenantsContext); const { currentTenant } = useContext(TenantsContext);
const { isLoading: isSubscriptionPlansLoading, data: fetchedPlans } = useSubscriptionPlans();
const { const {
data: currentSubscription, data: currentSubscription,
isLoading: isSubscriptionLoading, isLoading: isSubscriptionLoading,
mutate: mutateSubscription, mutate: mutateSubscription,
} = useSubscription(condString(currentTenant?.id)); } = useSubscription(condString(currentTenant?.id));
const subscriptionPlans = useMemo(() => cond(isCloud && fetchedPlans) ?? [], [fetchedPlans]);
const currentPlan = useMemo(
() =>
subscriptionPlans.find((plan) => plan.id === currentTenant?.planId) ??
defaultSubscriptionPlan,
[currentTenant?.planId, subscriptionPlans]
);
return { return {
isLoading: isSubscriptionLoading || isSubscriptionPlansLoading, isLoading: isSubscriptionLoading,
subscriptionPlans,
currentPlan,
currentSubscription: currentSubscription ?? defaultTenantResponse.subscription, currentSubscription: currentSubscription ?? defaultTenantResponse.subscription,
onCurrentSubscriptionUpdated: mutateSubscription, onCurrentSubscriptionUpdated: mutateSubscription,
}; };

View file

@ -1,50 +0,0 @@
import { type Optional } from '@silverhand/essentials';
import { useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
import { isCloud } from '@/consts/env';
import { featuredPlanIdOrder } from '@/consts/subscriptions';
// Used in the docs
// eslint-disable-next-line unused-imports/no-unused-imports
import TenantAccess from '@/containers/TenantAccess';
import { type SubscriptionPlan } from '@/types/subscriptions';
import { sortBy } from '@/utils/sort';
import { addSupportQuotaToPlan } from '@/utils/subscription';
/**
* @deprecated
* Fetch subscription plans from the cloud API.
* Note: If you want to retrieve subscription plans under the {@link TenantAccess} component, use `SubscriptionDataContext` instead.
*/
const useSubscriptionPlans = () => {
const cloudApi = useCloudApi();
const useSwrResponse = useSWRImmutable<SubscriptionPlanResponse[], Error>(
isCloud && '/api/subscription-plans',
async () => cloudApi.get('/api/subscription-plans')
);
const { data: subscriptionPlansResponse } = useSwrResponse;
const subscriptionPlans: Optional<SubscriptionPlan[]> = useMemo(() => {
if (!subscriptionPlansResponse) {
return;
}
return subscriptionPlansResponse
.map((plan) => addSupportQuotaToPlan(plan))
.slice()
.sort(({ id: previousId }, { id: nextId }) =>
sortBy(featuredPlanIdOrder)(previousId, nextId)
);
}, [subscriptionPlansResponse]);
return {
...useSwrResponse,
data: subscriptionPlans,
};
};
export default useSubscriptionPlans;

View file

@ -15,7 +15,6 @@ 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 useLogtoSkus from '@/hooks/use-logto-skus';
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
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';
@ -33,8 +32,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: subscriptionPlans, error: fetchPlansError } = useSubscriptionPlans();
const isLoadingPlans = !subscriptionPlans && !fetchPlansError;
const { data: logtoSkus, error: fetchLogtoSkusError } = useLogtoSkus(); const { data: logtoSkus, error: fetchLogtoSkusError } = useLogtoSkus();
const isLoadingLogtoSkus = !logtoSkus && !fetchLogtoSkusError; const isLoadingLogtoSkus = !logtoSkus && !fetchLogtoSkusError;
@ -132,12 +129,11 @@ function CheckoutSuccessCallback() {
navigate, navigate,
navigateTenant, navigateTenant,
onCurrentSubscriptionUpdated, onCurrentSubscriptionUpdated,
subscriptionPlans,
t, t,
tenantSubscription, tenantSubscription,
]); ]);
if (!isValidSession && !isLoadingPlans) { if (!isValidSession) {
return <Navigate replace to={consoleHomePage} />; return <Navigate replace to={consoleHomePage} />;
} }

View file

@ -23,15 +23,10 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
const { currentTenantId } = useContext(TenantsContext); const { currentTenantId } = useContext(TenantsContext);
const { subscribe } = useSubscribe(); const { subscribe } = useSubscribe();
const { show } = useConfirmModal(); const { show } = useConfirmModal();
const { subscriptionPlans, logtoSkus, currentSubscriptionQuota } = const { logtoSkus, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext); const { currentTenant } = useContext(TenantsContext);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const proPlan = useMemo(
() => subscriptionPlans.find(({ id }) => id === ReservedPlanId.Pro),
[subscriptionPlans]
);
const proSku = useMemo(() => logtoSkus.find(({ id }) => id === ReservedPlanId.Pro), [logtoSkus]); const proSku = useMemo(() => logtoSkus.find(({ id }) => id === ReservedPlanId.Pro), [logtoSkus]);
const periodicUsage = useMemo( const periodicUsage = useMemo(
@ -55,7 +50,6 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
if ( if (
mauLimit === null || // Unlimited mauLimit === null || // Unlimited
periodicUsage.mauLimit < mauLimit || periodicUsage.mauLimit < mauLimit ||
!proPlan ||
!proSku !proSku
) { ) {
return null; return null;
@ -72,7 +66,7 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
setIsLoading(true); setIsLoading(true);
await subscribe({ await subscribe({
skuId: proSku.id, skuId: proSku.id,
planId: proPlan.id, planId: proSku.id,
tenantId: currentTenantId, tenantId: currentTenantId,
callbackPage: subscriptionPage, callbackPage: subscriptionPage,
}); });

View file

@ -1,6 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { type InvoicesResponse, type SubscriptionPlanResponse } from '@/cloud/types/router'; import { type InvoicesResponse } from '@/cloud/types/router';
export enum ReservedPlanName { export enum ReservedPlanName {
Free = 'Free', Free = 'Free',
@ -19,18 +19,6 @@ export enum ReservedSkuId {
Enterprise = 'enterprise', Enterprise = 'enterprise',
} }
type SubscriptionPlanQuota = Omit<
SubscriptionPlanResponse['quota'],
'builtInEmailConnectorEnabled'
> & {
// Add ticket support quota item to the plan since it will be compared in the downgrade plan notification modal.
ticketSupportResponseTime: number;
};
export type SubscriptionPlan = Omit<SubscriptionPlanResponse, 'quota'> & {
quota: SubscriptionPlanQuota;
};
export const localCheckoutSessionGuard = z.object({ export const localCheckoutSessionGuard = z.object({
state: z.string(), state: z.string(),
sessionId: z.string(), sessionId: z.string(),

View file

@ -4,26 +4,11 @@ import { ResponseError } from '@withtyped/client';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api'; import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api';
import { type LogtoSkuResponse, type SubscriptionPlanResponse } from '@/cloud/types/router'; import { type LogtoSkuResponse } from '@/cloud/types/router';
import { ticketSupportResponseTimeMap } from '@/consts/plan-quotas'; 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 addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlanResponse) => {
const { id, quota } = subscriptionPlanResponse;
return {
...subscriptionPlanResponse,
quota: {
...quota,
/**
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
*/
ticketSupportResponseTime: ticketSupportResponseTimeMap[id] ?? 0, // Fallback to not supported
},
};
};
export const addSupportQuota = (logtoSkuResponse: LogtoSkuResponse) => { export const addSupportQuota = (logtoSkuResponse: LogtoSkuResponse) => {
const { id, quota } = logtoSkuResponse; const { id, quota } = logtoSkuResponse;