mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor(console): retrieve subscription data from tenant response (#4430)
refactor(console): apply tenant subscription data from tenant response
This commit is contained in:
parent
b3fc33524e
commit
1453e1a2a1
10 changed files with 102 additions and 152 deletions
|
@ -26,7 +26,7 @@
|
||||||
"@fontsource/roboto-mono": "^5.0.0",
|
"@fontsource/roboto-mono": "^5.0.0",
|
||||||
"@jest/types": "^29.5.0",
|
"@jest/types": "^29.5.0",
|
||||||
"@logto/app-insights": "workspace:^1.3.1",
|
"@logto/app-insights": "workspace:^1.3.1",
|
||||||
"@logto/cloud": "0.2.5-1795c3d",
|
"@logto/cloud": "0.2.5-71b7fea",
|
||||||
"@logto/connector-kit": "workspace:^1.1.1",
|
"@logto/connector-kit": "workspace:^1.1.1",
|
||||||
"@logto/core-kit": "workspace:^2.0.1",
|
"@logto/core-kit": "workspace:^2.0.1",
|
||||||
"@logto/language-kit": "workspace:^1.0.0",
|
"@logto/language-kit": "workspace:^1.0.0",
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@types/react-modal": "^3.13.1",
|
"@types/react-modal": "^3.13.1",
|
||||||
"@types/react-syntax-highlighter": "^15.5.1",
|
"@types/react-syntax-highlighter": "^15.5.1",
|
||||||
"@withtyped/client": "^0.7.21",
|
"@withtyped/client": "^0.7.22",
|
||||||
"buffer": "^5.7.1",
|
"buffer": "^5.7.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"clean-deep": "^3.4.0",
|
"clean-deep": "^3.4.0",
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
|
||||||
|
|
||||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
|
||||||
import PlanUsage from '@/components/PlanUsage';
|
import PlanUsage from '@/components/PlanUsage';
|
||||||
import { contactEmailLink } from '@/consts';
|
import { contactEmailLink } from '@/consts';
|
||||||
import { subscriptionPage } from '@/consts/pages';
|
import { subscriptionPage } from '@/consts/pages';
|
||||||
|
@ -21,35 +19,22 @@ import PlanName from '../PlanName';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
function MauExceededModal() {
|
function MauExceededModal() {
|
||||||
const { currentTenantId } = useContext(TenantsContext);
|
const { currentTenant } = useContext(TenantsContext);
|
||||||
const cloudApi = useCloudApi();
|
const { usage, subscription } = currentTenant ?? {};
|
||||||
|
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { navigate } = useTenantPathname();
|
const { navigate } = useTenantPathname();
|
||||||
const [hasClosed, setHasClosed] = useState(false);
|
|
||||||
|
|
||||||
|
const [hasClosed, setHasClosed] = useState(false);
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setHasClosed(true);
|
setHasClosed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data: subscriptionPlans } = useSubscriptionPlans();
|
const { data: subscriptionPlans } = useSubscriptionPlans();
|
||||||
|
|
||||||
const { data: currentSubscription } = useSWRImmutable(
|
const currentPlan = subscriptionPlans?.find((plan) => plan.id === subscription?.planId);
|
||||||
`/api/tenants/${currentTenantId}/subscription`,
|
|
||||||
async () =>
|
|
||||||
cloudApi.get('/api/tenants/:tenantId/subscription', { params: { tenantId: currentTenantId } })
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: currentUsage } = useSWRImmutable(
|
if (!subscription || !usage || !currentPlan || hasClosed) {
|
||||||
`/api/tenants/${currentTenantId}/usage`,
|
|
||||||
async () =>
|
|
||||||
cloudApi.get('/api/tenants/:tenantId/usage', { params: { tenantId: currentTenantId } })
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPlan =
|
|
||||||
currentSubscription &&
|
|
||||||
subscriptionPlans?.find((plan) => plan.id === currentSubscription.planId);
|
|
||||||
|
|
||||||
if (!currentPlan || !currentUsage || hasClosed) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +43,7 @@ function MauExceededModal() {
|
||||||
name: planName,
|
name: planName,
|
||||||
} = currentPlan;
|
} = currentPlan;
|
||||||
|
|
||||||
const isMauExceeded = mauLimit !== null && currentUsage.activeUsers >= mauLimit;
|
const isMauExceeded = mauLimit !== null && usage.activeUsers >= mauLimit;
|
||||||
|
|
||||||
if (!isMauExceeded) {
|
if (!isMauExceeded) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -102,8 +87,8 @@ function MauExceededModal() {
|
||||||
</InlineNotification>
|
</InlineNotification>
|
||||||
<FormField title="subscription.plan_usage">
|
<FormField title="subscription.plan_usage">
|
||||||
<PlanUsage
|
<PlanUsage
|
||||||
subscriptionUsage={currentUsage}
|
subscriptionUsage={usage}
|
||||||
currentSubscription={currentSubscription}
|
currentSubscription={subscription}
|
||||||
currentPlan={currentPlan}
|
currentPlan={currentPlan}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { useContext, useState } from 'react';
|
||||||
import { useContext, useMemo, useState } from 'react';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import ReactModal from 'react-modal';
|
import ReactModal from 'react-modal';
|
||||||
import useSWRImmutable from 'swr/immutable';
|
|
||||||
|
|
||||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
|
||||||
import { contactEmailLink } from '@/consts';
|
import { contactEmailLink } from '@/consts';
|
||||||
|
import { isCloud } from '@/consts/env';
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import Button from '@/ds-components/Button';
|
import Button from '@/ds-components/Button';
|
||||||
import FormField from '@/ds-components/FormField';
|
import FormField from '@/ds-components/FormField';
|
||||||
|
@ -13,7 +11,6 @@ import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
import ModalLayout from '@/ds-components/ModalLayout';
|
import ModalLayout from '@/ds-components/ModalLayout';
|
||||||
import useSubscribe from '@/hooks/use-subscribe';
|
import useSubscribe from '@/hooks/use-subscribe';
|
||||||
import * as modalStyles from '@/scss/modal.module.scss';
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
import { getLatestUnpaidInvoice } from '@/utils/subscription';
|
|
||||||
|
|
||||||
import BillInfo from '../BillInfo';
|
import BillInfo from '../BillInfo';
|
||||||
|
|
||||||
|
@ -22,26 +19,17 @@ import * as styles from './index.module.scss';
|
||||||
function PaymentOverdueModal() {
|
function PaymentOverdueModal() {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { currentTenant, currentTenantId } = useContext(TenantsContext);
|
const { currentTenant, currentTenantId } = useContext(TenantsContext);
|
||||||
const cloudApi = useCloudApi();
|
const { openInvoices = [] } = currentTenant ?? {};
|
||||||
const { data: invoicesResponse } = useSWRImmutable(
|
|
||||||
`/api/tenants/${currentTenantId}/invoices`,
|
|
||||||
async () =>
|
|
||||||
cloudApi.get('/api/tenants/:tenantId/invoices', { params: { tenantId: currentTenantId } })
|
|
||||||
);
|
|
||||||
const { visitManagePaymentPage } = useSubscribe();
|
const { visitManagePaymentPage } = useSubscribe();
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
|
|
||||||
const latestUnpaidInvoice = useMemo(
|
|
||||||
() => conditional(invoicesResponse && getLatestUnpaidInvoice(invoicesResponse.invoices)),
|
|
||||||
[invoicesResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [hasClosed, setHasClosed] = useState(false);
|
const [hasClosed, setHasClosed] = useState(false);
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
setHasClosed(true);
|
setHasClosed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!invoicesResponse || !latestUnpaidInvoice || hasClosed) {
|
if (!isCloud || openInvoices.length === 0 || hasClosed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +70,12 @@ function PaymentOverdueModal() {
|
||||||
</InlineNotification>
|
</InlineNotification>
|
||||||
)}
|
)}
|
||||||
<FormField title="upsell.payment_overdue_modal.unpaid_bills">
|
<FormField title="upsell.payment_overdue_modal.unpaid_bills">
|
||||||
<BillInfo cost={latestUnpaidInvoice.amountDue} />
|
<BillInfo
|
||||||
|
cost={openInvoices.reduce(
|
||||||
|
(total, currentInvoice) => total + currentInvoice.amountDue,
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</ModalLayout>
|
</ModalLayout>
|
||||||
</ReactModal>
|
</ReactModal>
|
||||||
|
|
|
@ -1,35 +1,16 @@
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
import Tag from '@/ds-components/Tag';
|
import Tag from '@/ds-components/Tag';
|
||||||
import useInvoices from '@/hooks/use-invoices';
|
import { type SubscriptionPlan } from '@/types/subscriptions';
|
||||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
|
||||||
import useSubscriptionUsage from '@/hooks/use-subscription-usage';
|
|
||||||
import { getLatestUnpaidInvoice } from '@/utils/subscription';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tenantId: string;
|
tenantData: TenantResponse;
|
||||||
|
tenantPlan: SubscriptionPlan;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function TenantStatusTag({ tenantId, className }: Props) {
|
function TenantStatusTag({ tenantData, tenantPlan, className }: Props) {
|
||||||
const { data: usage, error: fetchUsageError } = useSubscriptionUsage(tenantId);
|
const { usage, openInvoices } = tenantData;
|
||||||
const { data: invoices, error: fetchInvoiceError } = useInvoices(tenantId);
|
|
||||||
const { data: subscriptionPlan, error: fetchSubscriptionError } = useSubscriptionPlan(tenantId);
|
|
||||||
|
|
||||||
const isLoadingUsage = !usage && !fetchUsageError;
|
|
||||||
const isLoadingInvoice = !invoices && !fetchInvoiceError;
|
|
||||||
const isLoadingSubscription = !subscriptionPlan && !fetchSubscriptionError;
|
|
||||||
|
|
||||||
const latestUnpaidInvoice = useMemo(
|
|
||||||
() => conditional(invoices && getLatestUnpaidInvoice(invoices)),
|
|
||||||
[invoices]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoadingUsage || isLoadingInvoice || isLoadingSubscription) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tenant status priority:
|
* Tenant status priority:
|
||||||
|
@ -38,7 +19,7 @@ function TenantStatusTag({ tenantId, className }: Props) {
|
||||||
* 3. mau exceeded
|
* 3. mau exceeded
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (invoices && latestUnpaidInvoice) {
|
if (openInvoices.length > 0) {
|
||||||
return (
|
return (
|
||||||
<Tag className={className}>
|
<Tag className={className}>
|
||||||
<DynamicT forKey="tenants.status.overdue" />
|
<DynamicT forKey="tenants.status.overdue" />
|
||||||
|
@ -46,22 +27,20 @@ function TenantStatusTag({ tenantId, className }: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscriptionPlan && usage) {
|
const { activeUsers } = usage;
|
||||||
const { activeUsers } = usage;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
quota: { mauLimit },
|
quota: { mauLimit },
|
||||||
} = subscriptionPlan;
|
} = tenantPlan;
|
||||||
|
|
||||||
const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit;
|
const isMauExceeded = mauLimit !== null && activeUsers >= mauLimit;
|
||||||
|
|
||||||
if (isMauExceeded) {
|
if (isMauExceeded) {
|
||||||
return (
|
return (
|
||||||
<Tag className={className}>
|
<Tag className={className}>
|
||||||
<DynamicT forKey="tenants.status.mau_exceeded" />
|
<DynamicT forKey="tenants.status.mau_exceeded" />
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import Tick from '@/assets/icons/tick.svg';
|
import Tick from '@/assets/icons/tick.svg';
|
||||||
import { type TenantResponse } from '@/cloud/types/router';
|
import { type TenantResponse } from '@/cloud/types/router';
|
||||||
import PlanName from '@/components/PlanName';
|
import PlanName from '@/components/PlanName';
|
||||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
|
||||||
|
|
||||||
import TenantEnvTag from '../TenantEnvTag';
|
import TenantEnvTag from '../TenantEnvTag';
|
||||||
|
|
||||||
|
@ -18,8 +19,18 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
||||||
const { id, name, tag } = tenantData;
|
const {
|
||||||
const { data: tenantPlan } = useSubscriptionPlan(id);
|
name,
|
||||||
|
tag,
|
||||||
|
subscription: { planId },
|
||||||
|
} = tenantData;
|
||||||
|
|
||||||
|
const { data: plans } = useSubscriptionPlans();
|
||||||
|
const tenantPlan = useMemo(() => plans?.find((plan) => plan.id === planId), [plans, planId]);
|
||||||
|
|
||||||
|
if (!tenantPlan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownItem className={styles.item} onClick={onClick}>
|
<DropdownItem className={styles.item} onClick={onClick}>
|
||||||
|
@ -27,9 +38,15 @@ function TenantDropdownItem({ tenantData, isSelected, onClick }: Props) {
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
<div className={styles.name}>{name}</div>
|
<div className={styles.name}>{name}</div>
|
||||||
<TenantEnvTag tag={tag} />
|
<TenantEnvTag tag={tag} />
|
||||||
<TenantStatusTag tenantId={id} className={styles.statusTag} />
|
<TenantStatusTag
|
||||||
|
tenantData={tenantData}
|
||||||
|
tenantPlan={tenantPlan}
|
||||||
|
className={styles.statusTag}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.planName}>
|
||||||
|
<PlanName name={tenantPlan.name} />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.planName}>{tenantPlan && <PlanName name={tenantPlan.name} />}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Tick className={classNames(styles.checkIcon, isSelected && styles.visible)} />
|
<Tick className={classNames(styles.checkIcon, isSelected && styles.visible)} />
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
|
import { defaultManagementApi, defaultTenantId } from '@logto/schemas';
|
||||||
import { TenantTag } from '@logto/schemas/models';
|
import { TenantTag } from '@logto/schemas/models';
|
||||||
import { conditionalArray, noop } from '@silverhand/essentials';
|
import { conditionalArray, noop } from '@silverhand/essentials';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, createContext, useState } from 'react';
|
import { useCallback, useMemo, createContext, useState } from 'react';
|
||||||
import { useMatch, useNavigate } from 'react-router-dom';
|
import { useMatch, useNavigate } from 'react-router-dom';
|
||||||
|
@ -65,17 +66,27 @@ const { tenantId, indicator } = defaultManagementApi.resource;
|
||||||
* - For cloud, the initial tenants data is empty, and it will be fetched from the cloud API.
|
* - For cloud, the initial tenants data is empty, and it will be fetched from the cloud API.
|
||||||
* - OSS has a fixed tenant with ID `default` and no cloud API to dynamically fetch tenants.
|
* - OSS has a fixed tenant with ID `default` and no cloud API to dynamically fetch tenants.
|
||||||
*/
|
*/
|
||||||
const initialTenants = Object.freeze(
|
const defaultTenantResponse: TenantResponse = {
|
||||||
conditionalArray(
|
id: tenantId,
|
||||||
!isCloud && {
|
name: `tenant_${tenantId}`,
|
||||||
id: tenantId,
|
tag: TenantTag.Development,
|
||||||
name: `tenant_${tenantId}`,
|
indicator,
|
||||||
tag: TenantTag.Development,
|
subscription: {
|
||||||
indicator,
|
status: 'active',
|
||||||
planId: `${ReservedPlanId.free}`, // `planId` is string type.
|
planId: ReservedPlanId.free,
|
||||||
}
|
currentPeriodStart: dayjs().toDate(),
|
||||||
)
|
currentPeriodEnd: dayjs().add(1, 'month').toDate(),
|
||||||
);
|
},
|
||||||
|
usage: {
|
||||||
|
activeUsers: 0,
|
||||||
|
cost: 0,
|
||||||
|
},
|
||||||
|
openInvoices: [],
|
||||||
|
isSuspended: false,
|
||||||
|
planId: ReservedPlanId.free, // Reserved for compatibility with cloud
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialTenants = Object.freeze(conditionalArray(!isCloud && defaultTenantResponse));
|
||||||
|
|
||||||
export const TenantsContext = createContext<Tenants>({
|
export const TenantsContext = createContext<Tenants>({
|
||||||
tenants: initialTenants,
|
tenants: initialTenants,
|
||||||
|
|
|
@ -1,32 +1,29 @@
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { useContext, useState } from 'react';
|
||||||
import { useContext, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||||
import DynamicT from '@/ds-components/DynamicT';
|
import DynamicT from '@/ds-components/DynamicT';
|
||||||
import InlineNotification from '@/ds-components/InlineNotification';
|
import InlineNotification from '@/ds-components/InlineNotification';
|
||||||
import useInvoices from '@/hooks/use-invoices';
|
|
||||||
import useSubscribe from '@/hooks/use-subscribe';
|
import useSubscribe from '@/hooks/use-subscribe';
|
||||||
import { getLatestUnpaidInvoice } from '@/utils/subscription';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function PaymentOverdueNotification({ className }: Props) {
|
function PaymentOverdueNotification({ className }: Props) {
|
||||||
const { currentTenantId } = useContext(TenantsContext);
|
const { currentTenant, currentTenantId } = useContext(TenantsContext);
|
||||||
|
const { openInvoices = [] } = currentTenant ?? {};
|
||||||
const { visitManagePaymentPage } = useSubscribe();
|
const { visitManagePaymentPage } = useSubscribe();
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
const { data: invoices, error } = useInvoices(currentTenantId);
|
|
||||||
const isLoadingInvoices = !invoices && !error;
|
|
||||||
const latestUnpaidInvoice = useMemo(
|
|
||||||
() => conditional(invoices && getLatestUnpaidInvoice(invoices)),
|
|
||||||
[invoices]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoadingInvoices || !latestUnpaidInvoice) {
|
if (openInvoices.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalAmountDue = openInvoices.reduce(
|
||||||
|
(total, currentInvoice) => total + currentInvoice.amountDue,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineNotification
|
<InlineNotification
|
||||||
severity="error"
|
severity="error"
|
||||||
|
@ -41,7 +38,7 @@ function PaymentOverdueNotification({ className }: Props) {
|
||||||
>
|
>
|
||||||
<DynamicT
|
<DynamicT
|
||||||
forKey="subscription.payment_error"
|
forKey="subscription.payment_error"
|
||||||
interpolation={{ price: latestUnpaidInvoice.amountDue / 100 }}
|
interpolation={{ price: totalAmountDue / 100 }}
|
||||||
/>
|
/>
|
||||||
</InlineNotification>
|
</InlineNotification>
|
||||||
);
|
);
|
||||||
|
|
|
@ -76,6 +76,4 @@ export const localCheckoutSessionGuard = z.object({
|
||||||
|
|
||||||
export type LocalCheckoutSession = z.infer<typeof localCheckoutSessionGuard>;
|
export type LocalCheckoutSession = z.infer<typeof localCheckoutSessionGuard>;
|
||||||
|
|
||||||
export type Invoice = InvoicesResponse['invoices'][number];
|
export type InvoiceStatus = InvoicesResponse['invoices'][number]['status'];
|
||||||
|
|
||||||
export type InvoiceStatus = Invoice['status'];
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api';
|
||||||
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
|
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
|
||||||
import { communitySupportEnabledMap, ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
|
import { communitySupportEnabledMap, ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
|
||||||
import { reservedPlanIdOrder } from '@/consts/subscriptions';
|
import { reservedPlanIdOrder } from '@/consts/subscriptions';
|
||||||
import { type Invoice } from '@/types/subscriptions';
|
|
||||||
|
|
||||||
export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlanResponse) => {
|
export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlanResponse) => {
|
||||||
const { id, quota } = subscriptionPlanResponse;
|
const { id, quota } = subscriptionPlanResponse;
|
||||||
|
@ -43,15 +42,6 @@ export const formatPeriod = ({ periodStart, periodEnd, displayYear }: FormatPeri
|
||||||
return `${formattedStart} - ${formattedEnd}`;
|
return `${formattedStart} - ${formattedEnd}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLatestUnpaidInvoice = (invoices: Invoice[]) =>
|
|
||||||
invoices
|
|
||||||
.slice()
|
|
||||||
.sort(
|
|
||||||
(invoiceA, invoiceB) =>
|
|
||||||
new Date(invoiceB.createdAt).getTime() - new Date(invoiceA.createdAt).getTime()
|
|
||||||
)
|
|
||||||
.find(({ status }) => status === 'uncollectible');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: this is a temporary solution to handle the case when the user tries to downgrade but the quota limit is exceeded.
|
* Note: this is a temporary solution to handle the case when the user tries to downgrade but the quota limit is exceeded.
|
||||||
* Need a better solution to handle this case by sharing the error type between the console and cloud. - LOG-6608
|
* Need a better solution to handle this case by sharing the error type between the console and cloud. - LOG-6608
|
||||||
|
|
|
@ -2834,8 +2834,8 @@ importers:
|
||||||
specifier: workspace:^1.3.1
|
specifier: workspace:^1.3.1
|
||||||
version: link:../app-insights
|
version: link:../app-insights
|
||||||
'@logto/cloud':
|
'@logto/cloud':
|
||||||
specifier: 0.2.5-1795c3d
|
specifier: 0.2.5-71b7fea
|
||||||
version: 0.2.5-1795c3d(zod@3.20.2)
|
version: 0.2.5-71b7fea(zod@3.20.2)
|
||||||
'@logto/connector-kit':
|
'@logto/connector-kit':
|
||||||
specifier: workspace:^1.1.1
|
specifier: workspace:^1.1.1
|
||||||
version: link:../toolkit/connector-kit
|
version: link:../toolkit/connector-kit
|
||||||
|
@ -2936,8 +2936,8 @@ importers:
|
||||||
specifier: ^15.5.1
|
specifier: ^15.5.1
|
||||||
version: 15.5.1
|
version: 15.5.1
|
||||||
'@withtyped/client':
|
'@withtyped/client':
|
||||||
specifier: ^0.7.21
|
specifier: ^0.7.22
|
||||||
version: 0.7.21(zod@3.20.2)
|
version: 0.7.22(zod@3.20.2)
|
||||||
buffer:
|
buffer:
|
||||||
specifier: ^5.7.1
|
specifier: ^5.7.1
|
||||||
version: 5.7.1
|
version: 5.7.1
|
||||||
|
@ -7330,12 +7330,12 @@ packages:
|
||||||
jose: 4.14.4
|
jose: 4.14.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@logto/cloud@0.2.5-1795c3d(zod@3.20.2):
|
/@logto/cloud@0.2.5-71b7fea(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-zxy9zr5swOxbzYJNYtKXofj2tSIS9565d+1pT6RSbmx3Hn+JG6uzsb75PZXW9vlmmm7p1sGZeTQ+xVzKNFPsMg==}
|
resolution: {integrity: sha512-howllmEV6kWAgusP+2OSloG5bZQ146UiKn0PpA7xi9HcpgM6Fd1NPuNjc3BZdInJ5Qn0en6LOZL7c2EwTRx3jw==}
|
||||||
engines: {node: ^18.12.0}
|
engines: {node: ^18.12.0}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@silverhand/essentials': 2.8.4
|
'@silverhand/essentials': 2.8.4
|
||||||
'@withtyped/server': 0.12.8(zod@3.20.2)
|
'@withtyped/server': 0.12.9(zod@3.20.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -10048,15 +10048,6 @@ packages:
|
||||||
eslint-visitor-keys: 3.4.1
|
eslint-visitor-keys: 3.4.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@withtyped/client@0.7.21(zod@3.20.2):
|
|
||||||
resolution: {integrity: sha512-N9dvH5nqIwaT7YxaIm83RRQf9AEjxwJ4ugJviZJSxtWy8zLul2/odEMc6epieylFVa6CcLg82yJmRSlqPtJiTw==}
|
|
||||||
dependencies:
|
|
||||||
'@withtyped/server': 0.12.8(zod@3.20.2)
|
|
||||||
'@withtyped/shared': 0.2.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- zod
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@withtyped/client@0.7.22(zod@3.20.2):
|
/@withtyped/client@0.7.22(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-emNtcO0jc0dFWhvL7eUIRYzhTfn+JqgIvCmXb8ZUFOR8wdSSGrr9VDlm+wgQD06DEBBpmqtTHMMHTNXJdUC/Qw==}
|
resolution: {integrity: sha512-emNtcO0jc0dFWhvL7eUIRYzhTfn+JqgIvCmXb8ZUFOR8wdSSGrr9VDlm+wgQD06DEBBpmqtTHMMHTNXJdUC/Qw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10064,17 +10055,6 @@ packages:
|
||||||
'@withtyped/shared': 0.2.2
|
'@withtyped/shared': 0.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- zod
|
- zod
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@withtyped/server@0.12.8(zod@3.20.2):
|
|
||||||
resolution: {integrity: sha512-fv9feTOKJhtlaoYM/Kbs2gSTcIXlmu4OMUFwGmK5jqdbVNIOkDBIPxtcC5ZEwevWFgOcd5OqBW+FvbjiaF27Fw==}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.19.1
|
|
||||||
dependencies:
|
|
||||||
'@silverhand/essentials': 2.8.4
|
|
||||||
'@withtyped/shared': 0.2.2
|
|
||||||
zod: 3.20.2
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@withtyped/server@0.12.9(zod@3.20.2):
|
/@withtyped/server@0.12.9(zod@3.20.2):
|
||||||
resolution: {integrity: sha512-K5zoV9D+WpawbghtlJKF1KOshKkBjq+gYzNRWuZk13YmFWFLcmZn+QCblNP55z9IGdcHWpTRknqb1APuicdzgA==}
|
resolution: {integrity: sha512-K5zoV9D+WpawbghtlJKF1KOshKkBjq+gYzNRWuZk13YmFWFLcmZn+QCblNP55z9IGdcHWpTRknqb1APuicdzgA==}
|
||||||
|
|
Loading…
Reference in a new issue