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:
parent
fa6f8ef498
commit
e54baf458a
10 changed files with 10 additions and 163 deletions
|
@ -7,10 +7,6 @@ type GetTenantAuthRoutes = RouterRoutes<typeof tenantAuthRouter>['get'];
|
|||
|
||||
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 Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/subscription']>;
|
||||
|
|
|
@ -9,7 +9,6 @@ import {
|
|||
} from '@/cloud/types/router';
|
||||
import { RegionName } from '@/components/Region';
|
||||
import { LogtoSkuType } from '@/types/skus';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
import { adminEndpoint, isCloud } from './env';
|
||||
|
||||
|
@ -48,44 +47,6 @@ export const defaultTenantResponse: TenantResponse = {
|
|||
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.
|
||||
* - OSS has a fixed subscription plan with `development` id and no cloud API to dynamically fetch the subscription plan.
|
||||
|
|
|
@ -2,7 +2,6 @@ import { noop } from '@silverhand/essentials';
|
|||
import { createContext, type ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
defaultSubscriptionPlan,
|
||||
defaultLogtoSku,
|
||||
defaultTenantResponse,
|
||||
defaultSubscriptionQuota,
|
||||
|
@ -21,8 +20,6 @@ const defaultSubscription = defaultTenantResponse.subscription;
|
|||
* CAUTION: You should only use this data context under the {@link TenantAccess} component
|
||||
*/
|
||||
export const SubscriptionDataContext = createContext<FullContext>({
|
||||
subscriptionPlans: [],
|
||||
currentPlan: defaultSubscriptionPlan,
|
||||
currentSubscription: defaultSubscription,
|
||||
onCurrentSubscriptionUpdated: noop,
|
||||
/* ==== For new pricing model ==== */
|
||||
|
|
|
@ -6,13 +6,8 @@ import {
|
|||
type NewSubscriptionResourceScopeUsage,
|
||||
type NewSubscriptionRoleScopeUsage,
|
||||
} from '@/cloud/types/router';
|
||||
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
export type Context = {
|
||||
/** @deprecated */
|
||||
subscriptionPlans: SubscriptionPlan[];
|
||||
/** @deprecated */
|
||||
currentPlan: SubscriptionPlan;
|
||||
currentSubscription: Subscription;
|
||||
onCurrentSubscriptionUpdated: (subscription?: Subscription) => void;
|
||||
};
|
||||
|
@ -28,7 +23,6 @@ type NewSubscriptionSupplementContext = {
|
|||
mutateSubscriptionQuotaAndUsages: () => void;
|
||||
};
|
||||
|
||||
export type NewSubscriptionContext = Omit<Context, 'subscriptionPlans' | 'currentPlan'> &
|
||||
NewSubscriptionSupplementContext;
|
||||
export type NewSubscriptionContext = Context & NewSubscriptionSupplementContext;
|
||||
|
||||
export type FullContext = Context & NewSubscriptionSupplementContext;
|
||||
|
|
|
@ -1,37 +1,23 @@
|
|||
import { cond, condString } from '@silverhand/essentials';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { condString } from '@silverhand/essentials';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { defaultSubscriptionPlan, defaultTenantResponse } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { defaultTenantResponse } from '@/consts';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
||||
import useSubscription from '../../hooks/use-subscription';
|
||||
import useSubscriptionPlans from '../../hooks/use-subscription-plans';
|
||||
|
||||
import { type Context } from './types';
|
||||
|
||||
const useSubscriptionData: () => Context & { isLoading: boolean } = () => {
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
const { isLoading: isSubscriptionPlansLoading, data: fetchedPlans } = useSubscriptionPlans();
|
||||
const {
|
||||
data: currentSubscription,
|
||||
isLoading: isSubscriptionLoading,
|
||||
mutate: mutateSubscription,
|
||||
} = 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 {
|
||||
isLoading: isSubscriptionLoading || isSubscriptionPlansLoading,
|
||||
subscriptionPlans,
|
||||
currentPlan,
|
||||
isLoading: isSubscriptionLoading,
|
||||
currentSubscription: currentSubscription ?? defaultTenantResponse.subscription,
|
||||
onCurrentSubscriptionUpdated: mutateSubscription,
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -15,7 +15,6 @@ 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';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import { clearLocalCheckoutSession, getLocalCheckoutSession } from '@/utils/checkout';
|
||||
|
||||
|
@ -33,8 +32,6 @@ function CheckoutSuccessCallback() {
|
|||
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);
|
||||
const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {};
|
||||
|
||||
const { data: subscriptionPlans, error: fetchPlansError } = useSubscriptionPlans();
|
||||
const isLoadingPlans = !subscriptionPlans && !fetchPlansError;
|
||||
const { data: logtoSkus, error: fetchLogtoSkusError } = useLogtoSkus();
|
||||
const isLoadingLogtoSkus = !logtoSkus && !fetchLogtoSkusError;
|
||||
|
||||
|
@ -132,12 +129,11 @@ function CheckoutSuccessCallback() {
|
|||
navigate,
|
||||
navigateTenant,
|
||||
onCurrentSubscriptionUpdated,
|
||||
subscriptionPlans,
|
||||
t,
|
||||
tenantSubscription,
|
||||
]);
|
||||
|
||||
if (!isValidSession && !isLoadingPlans) {
|
||||
if (!isValidSession) {
|
||||
return <Navigate replace to={consoleHomePage} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,15 +23,10 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
|
|||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { subscribe } = useSubscribe();
|
||||
const { show } = useConfirmModal();
|
||||
const { subscriptionPlans, logtoSkus, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { logtoSkus, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
|
||||
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 periodicUsage = useMemo(
|
||||
|
@ -55,7 +50,6 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
|
|||
if (
|
||||
mauLimit === null || // Unlimited
|
||||
periodicUsage.mauLimit < mauLimit ||
|
||||
!proPlan ||
|
||||
!proSku
|
||||
) {
|
||||
return null;
|
||||
|
@ -72,7 +66,7 @@ function MauLimitExceededNotification({ periodicUsage: rawPeriodicUsage, classNa
|
|||
setIsLoading(true);
|
||||
await subscribe({
|
||||
skuId: proSku.id,
|
||||
planId: proPlan.id,
|
||||
planId: proSku.id,
|
||||
tenantId: currentTenantId,
|
||||
callbackPage: subscriptionPage,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { type InvoicesResponse, type SubscriptionPlanResponse } from '@/cloud/types/router';
|
||||
import { type InvoicesResponse } from '@/cloud/types/router';
|
||||
|
||||
export enum ReservedPlanName {
|
||||
Free = 'Free',
|
||||
|
@ -19,18 +19,6 @@ export enum ReservedSkuId {
|
|||
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({
|
||||
state: z.string(),
|
||||
sessionId: z.string(),
|
||||
|
|
|
@ -4,26 +4,11 @@ import { ResponseError } from '@withtyped/client';
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
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 { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions';
|
||||
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) => {
|
||||
const { id, quota } = logtoSkuResponse;
|
||||
|
||||
|
|
Loading…
Reference in a new issue