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 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']>;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 ==== */
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 { 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} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue