mirror of
https://github.com/labring/sealos.git
synced 2026-08-29 01:39:49 +08:00
fix(desktop): plan payment options unexpectedly exposed to non-owner users (#6752)
Signed-off-by: Nixieboluo <me@sagirii.me>
This commit is contained in:
@@ -55,7 +55,8 @@
|
||||
"trial_expiry_upgrade_tip_one": "Your trial will expire in {{count}} day. Upgrade to keep your services online.",
|
||||
"trial_expiry_upgrade_tip_other": "Your trial will expire in {{count}} days. Upgrade to keep your services online.",
|
||||
"upgrade_button": "Upgrade Plan",
|
||||
"upgrade_tip": "To upgrade your plan, you can visit the Cost Center."
|
||||
"upgrade_tip": "To upgrade your plan, you can visit the Cost Center.",
|
||||
"can_not_manage_payments": "Please contact the namespace owner to update or cancel the subscription."
|
||||
},
|
||||
"balance_text": "Balance",
|
||||
"bank_name": "Bank",
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
"trial_expiry_upgrade_tip_one": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.",
|
||||
"trial_expiry_upgrade_tip_other": "你的试用将在 {{count}} 天后过期. 升级套餐以保证服务在线.",
|
||||
"upgrade_button": "升级套餐",
|
||||
"upgrade_tip": "跳转到费用中心以升级套餐"
|
||||
"upgrade_tip": "跳转到费用中心以升级套餐",
|
||||
"can_not_manage_payments": "请联系空间管理员执行取消或升级订阅操作"
|
||||
},
|
||||
"balance_text": "余额",
|
||||
"bank_name": "开户银行",
|
||||
|
||||
@@ -95,6 +95,7 @@ export default function SecondaryLinks() {
|
||||
const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free';
|
||||
const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan;
|
||||
const isDebt = subscription?.Status?.toLowerCase() === 'debt';
|
||||
const canManagePayment = subscription?.role === 'OWNER';
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
@@ -126,7 +127,7 @@ export default function SecondaryLinks() {
|
||||
<Flex gap={'4px'} ml={'auto'}>
|
||||
<BalancePopover
|
||||
openCostCenterApp={() =>
|
||||
layoutConfig?.common.subscriptionEnabled
|
||||
layoutConfig?.common.subscriptionEnabled && canManagePayment
|
||||
? openCostCenterApp('upgrade')
|
||||
: openCostCenterApp()
|
||||
}
|
||||
@@ -144,7 +145,7 @@ export default function SecondaryLinks() {
|
||||
fontWeight={'500'}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
layoutConfig?.common.subscriptionEnabled
|
||||
layoutConfig?.common.subscriptionEnabled && canManagePayment
|
||||
? openCostCenterApp('upgrade')
|
||||
: openCostCenterApp()
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import useSessionStore from '@/stores/session';
|
||||
import { useSubscriptionStore } from '@/stores/subscription';
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import { Button, cn, Separator } from '@sealos/shadcn-ui';
|
||||
import { Tooltip, Button, cn, Separator, TooltipContent, TooltipTrigger } from '@sealos/shadcn-ui';
|
||||
import { WorkspaceSubscription } from '@/types/plan';
|
||||
import Decimal from 'decimal.js';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@@ -75,6 +75,8 @@ export function BalancePopover({
|
||||
const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free';
|
||||
const isCancelled = !!subscription?.CancelAtPeriodEnd && !isFreePlan;
|
||||
|
||||
const canManagePayment = subscription?.role === 'OWNER';
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return 'N/A';
|
||||
return new Date(dateStr).toLocaleString('zh-CN', {
|
||||
@@ -255,10 +257,24 @@ export function BalancePopover({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button variant="outline" onClick={openCostCenterApp}>
|
||||
<Sparkles size={16} />
|
||||
{t('common:balance_popover.upgrade_button')}
|
||||
</Button>
|
||||
<Tooltip open={canManagePayment ? false : undefined}>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
onClick={openCostCenterApp}
|
||||
disabled={!canManagePayment}
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
{t('common:balance_popover.upgrade_button')}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common:balance_popover.can_not_manage_payments')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
import { getAmount } from '@/api/auth';
|
||||
import { getUserBilling } from '@/api/platform';
|
||||
import useAppStore from '@/stores/app';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import useSessionStore from '@/stores/session';
|
||||
import { formatMoney } from '@/utils/format';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
Text,
|
||||
useBreakpointValue
|
||||
} from '@chakra-ui/react';
|
||||
import { CurrencySymbol, MonitorIcon } from '@sealos/ui';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Decimal } from 'decimal.js';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import CustomTooltip from '../AppDock/CustomTooltip';
|
||||
import { blurBackgroundStyles } from '../desktop_content';
|
||||
import Monitor from '../desktop_content/monitor';
|
||||
import { ClockIcon, HelpIcon, InfiniteIcon } from '../icons';
|
||||
import { BalancePopover } from './BalancePopover';
|
||||
|
||||
/**
|
||||
* @deprecated This component is deprecated. Use Cost Center to monitor balance instead.
|
||||
*/
|
||||
export default function Cost() {
|
||||
const { t } = useTranslation();
|
||||
const rechargeEnabled = useConfigStore().commonConfig?.rechargeEnabled;
|
||||
const openApp = useAppStore((s) => s.openApp);
|
||||
const installApp = useAppStore((s) => s.installedApps);
|
||||
const { session } = useSessionStore();
|
||||
const user = session?.user;
|
||||
const isLargerThanXl = useBreakpointValue({ base: true, xl: false });
|
||||
const currencySymbol = useConfigStore(
|
||||
(state) => state.layoutConfig?.currencySymbol || 'shellCoin'
|
||||
);
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['getAmount', { userId: user?.userCrUid }],
|
||||
queryFn: getAmount,
|
||||
enabled: !!user,
|
||||
staleTime: 60 * 1000
|
||||
});
|
||||
|
||||
const { data: billing } = useQuery(['getUserBilling'], () => getUserBilling(), {
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
const balance = useMemo(() => {
|
||||
let realBalance = new Decimal(data?.data?.balance || 0);
|
||||
if (data?.data?.deductionBalance) {
|
||||
realBalance = realBalance.minus(new Decimal(data.data.deductionBalance));
|
||||
}
|
||||
return realBalance.toNumber();
|
||||
}, [data]);
|
||||
|
||||
const calculations = useMemo(() => {
|
||||
const prevDayAmount = new Decimal(billing?.data?.prevDayTime || 0);
|
||||
const estimatedNextMonthAmount = prevDayAmount.times(30).toNumber();
|
||||
const _balance = new Decimal(balance || 0);
|
||||
|
||||
let estimatedDaysUsable;
|
||||
if (_balance.isNegative()) {
|
||||
estimatedDaysUsable = 0;
|
||||
} else if (prevDayAmount.isZero()) {
|
||||
estimatedDaysUsable = Number.POSITIVE_INFINITY;
|
||||
} else {
|
||||
estimatedDaysUsable = _balance.div(prevDayAmount).ceil().toNumber();
|
||||
}
|
||||
|
||||
return {
|
||||
prevMonthAmount: new Decimal(billing?.data?.prevMonthTime || 0).toNumber(),
|
||||
estimatedNextMonthAmount,
|
||||
estimatedDaysUsable
|
||||
};
|
||||
}, [billing?.data?.prevDayTime, billing?.data?.prevMonthTime, , balance]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
flex={'0 1 400px'}
|
||||
overflowY={'auto'}
|
||||
style={{
|
||||
scrollbarWidth: 'none'
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
{...blurBackgroundStyles}
|
||||
position={'relative'}
|
||||
zIndex={2}
|
||||
fontSize={'base'}
|
||||
fontWeight={'bold'}
|
||||
px={'16px'}
|
||||
pt={'20px'}
|
||||
flexDirection={'column'}
|
||||
>
|
||||
<BalancePopover
|
||||
openCostCenterApp={() => {
|
||||
const costcenter = installApp.find((t) => t.key === 'system-costcenter');
|
||||
if (!costcenter) return;
|
||||
openApp(costcenter);
|
||||
}}
|
||||
openCostCenterTopup={() => {}}
|
||||
>
|
||||
<Flex
|
||||
borderRadius={'6px'}
|
||||
p="16px"
|
||||
bg={'rgba(255, 255, 255, 0.05)'}
|
||||
justifyContent={'space-between'}
|
||||
_hover={{
|
||||
background: 'rgba(255, 255, 255, 0.10)'
|
||||
}}
|
||||
cursor="pointer"
|
||||
>
|
||||
<Box flex={1}>
|
||||
<Text color={'rgba(255, 255, 255, 0.90)'} fontSize={'11px'}>
|
||||
{t('common:balance_popover.balance')}
|
||||
</Text>
|
||||
<Flex alignItems={'center'} gap={'8px'}>
|
||||
<Text fontSize={'20px'} color={'#7CE7FF'}>
|
||||
{formatMoney(balance).toFixed(2)}
|
||||
</Text>
|
||||
<CurrencySymbol type={currencySymbol} color={'white'} fontSize={'16px'} />
|
||||
</Flex>
|
||||
</Box>
|
||||
{rechargeEnabled && (
|
||||
<Center
|
||||
ml="auto"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const costcenter = installApp.find((t) => t.key === 'system-costcenter');
|
||||
if (!costcenter) return;
|
||||
openApp(costcenter, {
|
||||
query: {
|
||||
openRecharge: 'true'
|
||||
}
|
||||
});
|
||||
}}
|
||||
color={'rgba(255, 255, 255, 0.90)'}
|
||||
cursor={'pointer'}
|
||||
>
|
||||
{t('common:charge')}
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
</BalancePopover>
|
||||
{calculations && (
|
||||
<Flex flexDirection={'column'}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={'16px'}
|
||||
py={'18px'}
|
||||
borderBottom={'1px solid rgba(255, 255, 255, 0.05)'}
|
||||
>
|
||||
<ClockIcon mr={'4px'} />
|
||||
<Text fontSize={'12px'} fontWeight={'bold'} color={'rgba(255, 255, 255, 0.90)'}>
|
||||
{t('common:expected_used')}
|
||||
</Text>
|
||||
<Text mr={'4px'} ml={'auto'} color={'white'} fontSize={'14px'} fontWeight={700}>
|
||||
{calculations.estimatedDaysUsable === Number.POSITIVE_INFINITY ? (
|
||||
<>
|
||||
<InfiniteIcon /> {t('common:day')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{calculations.estimatedDaysUsable} {t('common:day')}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
px={'16px'}
|
||||
py={'18px'}
|
||||
borderBottom={'1px solid rgba(255, 255, 255, 0.05)'}
|
||||
>
|
||||
<Center
|
||||
mr={'4px'}
|
||||
w={'7px'}
|
||||
height={'7px'}
|
||||
bg={'#4DB4FF'}
|
||||
borderRadius={'2px'}
|
||||
></Center>
|
||||
<Text fontSize={'12px'} fontWeight={'bold'} color={'rgba(255, 255, 255, 0.90)'}>
|
||||
{t('common:used_last_month')}
|
||||
</Text>
|
||||
<Text mr={'4px'} ml={'auto'} color={'white'} fontSize={'14px'} fontWeight={700}>
|
||||
{formatMoney(calculations.prevMonthAmount).toFixed(2)}
|
||||
</Text>
|
||||
<CurrencySymbol type={currencySymbol} color={'white'} fontSize={'14px'} />
|
||||
</Flex>
|
||||
<Flex alignItems={'center'} px={'16px'} py={'18px'}>
|
||||
<Center
|
||||
mr={'4px'}
|
||||
w={'7px'}
|
||||
height={'7px'}
|
||||
bg={'#C74FFF'}
|
||||
borderRadius={'2px'}
|
||||
></Center>
|
||||
<Flex alignItems={'center'} gap={'4px'} position={'relative'}>
|
||||
<Text fontSize={'12px'} fontWeight={'bold'} color={'rgba(255, 255, 255, 0.90)'}>
|
||||
{t('common:expected_to_use_next_month')}
|
||||
</Text>
|
||||
<CustomTooltip placement="bottom" label={t('common:amount_forecast')}>
|
||||
<Box cursor={'pointer'}>
|
||||
<HelpIcon />
|
||||
</Box>
|
||||
</CustomTooltip>
|
||||
</Flex>
|
||||
<Text mr={'4px'} ml={'auto'} color={'white'} fontSize={'14px'} fontWeight={700}>
|
||||
{formatMoney(calculations.estimatedNextMonthAmount).toFixed(2)}
|
||||
</Text>
|
||||
<CurrencySymbol type={currencySymbol} color={'white'} fontSize={'14px'} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{isLargerThanXl && (
|
||||
<Accordion allowMultiple mt={'8px'}>
|
||||
<AccordionItem py={'19px'} {...blurBackgroundStyles}>
|
||||
<AccordionButton
|
||||
gap={'6px'}
|
||||
_hover={{
|
||||
bg: ''
|
||||
}}
|
||||
>
|
||||
<MonitorIcon />
|
||||
<Text color={'rgba(255, 255, 255, 0.90)'} fontWeight={'bold'} fontSize={'14px'}>
|
||||
{t('common:monitor')}
|
||||
</Text>
|
||||
<AccordionIcon ml={'auto'} color={'white'} />
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel p={0}>
|
||||
<Monitor needStyles={false} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,9 @@ export const OperatorSchema = z.enum([
|
||||
]);
|
||||
export type Operator = z.infer<typeof OperatorSchema>;
|
||||
|
||||
export const WorkspaceRoleSchema = z.enum(['MANAGER', 'DEVELOPER', 'OWNER']);
|
||||
export type WorkspaceRole = z.infer<typeof WorkspaceRoleSchema>;
|
||||
|
||||
export const StripeInfoSchema = z.object({
|
||||
subscriptionId: z.string(),
|
||||
customerId: z.string()
|
||||
@@ -68,7 +71,8 @@ export const WorkspaceSubscriptionSchema = z.object({
|
||||
UpdateAt: z.string(),
|
||||
ExpireAt: z.string().nullable(),
|
||||
Traffic: z.array(z.any()).nullable(),
|
||||
type: SubscriptionTypeSchema
|
||||
type: SubscriptionTypeSchema,
|
||||
role: WorkspaceRoleSchema
|
||||
});
|
||||
export type WorkspaceSubscription = z.infer<typeof WorkspaceSubscriptionSchema>;
|
||||
|
||||
|
||||
@@ -445,5 +445,6 @@
|
||||
"jcb": "JCB",
|
||||
"unionpay": "UnionPay"
|
||||
},
|
||||
"downgrade_warning_message": "Please ensure resources remain within {{planName}} plan limits by {{date}} to avoid charges."
|
||||
"downgrade_warning_message": "Please ensure resources remain within {{planName}} plan limits by {{date}} to avoid charges.",
|
||||
"can_not_manage_payments": "Please contact the namespace owner to update or cancel the subscription."
|
||||
}
|
||||
|
||||
@@ -442,5 +442,6 @@
|
||||
"jcb": "JCB",
|
||||
"unionpay": "银联"
|
||||
},
|
||||
"downgrade_warning_message": "请确保资源在 {{date}} 之前保持在 {{planName}} 套餐限制内,以避免产生费用。"
|
||||
"downgrade_warning_message": "请确保资源在 {{date}} 之前保持在 {{planName}} 套餐限制内,以避免产生费用。",
|
||||
"can_not_manage_payments": "请联系空间管理员执行取消或升级订阅操作"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from '@sealos/shadcn-ui';
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from '@sealos/shadcn-ui';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { getCardInfo, createCardManageSession } from '@/api/plan';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -8,6 +8,7 @@ import useBillingStore from '@/stores/billing';
|
||||
import { BankCardIcon } from '../BankCardIcon';
|
||||
import { BankCardBrand } from '../BankCardBrand';
|
||||
import { openInNewWindow } from '@/utils/windowUtils';
|
||||
import usePlanStore from '@/stores/plan';
|
||||
|
||||
interface CardInfoSectionProps {
|
||||
workspace?: string;
|
||||
@@ -21,6 +22,9 @@ export function CardInfoSection({ workspace, regionDomain }: CardInfoSectionProp
|
||||
const { getRegion } = useBillingStore();
|
||||
const region = getRegion();
|
||||
|
||||
const subscriptionData = usePlanStore((state) => state.subscriptionData);
|
||||
const canManagePayment = subscriptionData?.subscription.role === 'OWNER';
|
||||
|
||||
const effectiveWorkspace = workspace || session?.user?.nsid || '';
|
||||
const effectiveRegionDomain = regionDomain || region?.domain || '';
|
||||
|
||||
@@ -133,14 +137,25 @@ export function CardInfoSection({ workspace, regionDomain }: CardInfoSectionProp
|
||||
</div>
|
||||
|
||||
{hasCard && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleManageCards}
|
||||
disabled={manageCardMutation.isLoading}
|
||||
className="h-10"
|
||||
>
|
||||
{manageCardMutation.isLoading ? t('common:loading') : t('common:manage_card_info')}
|
||||
</Button>
|
||||
<Tooltip open={canManagePayment ? false : undefined}>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleManageCards}
|
||||
disabled={manageCardMutation.isLoading || !canManagePayment}
|
||||
className="h-10"
|
||||
>
|
||||
{manageCardMutation.isLoading
|
||||
? t('common:loading')
|
||||
: t('common:manage_card_info')}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common:can_not_manage_payments')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +54,7 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) {
|
||||
|
||||
const plans = plansData?.plans;
|
||||
const subscription = subscriptionData?.subscription;
|
||||
const canManagePayment = subscription?.role === 'OWNER';
|
||||
const lastTransaction = lastTransactionData?.transaction;
|
||||
const planName = subscription?.PlanName || t('common:free_plan');
|
||||
const isFreePlan = (subscription?.PlanName || '').toLowerCase() === 'free';
|
||||
@@ -196,9 +197,16 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) {
|
||||
|
||||
{children?.({
|
||||
trigger: (
|
||||
<Button size="lg" variant="outline">
|
||||
<span>{inDebt ? t('common:renew') : t('common:subscribe_plan')}</span>
|
||||
</Button>
|
||||
<Tooltip open={canManagePayment ? false : undefined}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button size="lg" variant="outline" disabled={!canManagePayment}>
|
||||
<span>{inDebt ? t('common:renew') : t('common:subscribe_plan')}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common:can_not_manage_payments')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -222,74 +230,81 @@ export function PlanHeader({ children, onRenewSuccess }: PlanHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isNormal && !isFreePlan && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
disabled={isCancelled}
|
||||
onClick={() => {
|
||||
if (isCancelled) return;
|
||||
setCancelModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span>{isCancelled ? t('common:cancelled') : t('common:cancel_plan')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{isCancelled && (
|
||||
<Button
|
||||
size="lg"
|
||||
disabled={resumePlanMutation.isLoading}
|
||||
onClick={() => {
|
||||
if (!subscription) return;
|
||||
<Tooltip open={canManagePayment ? false : undefined}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-3">
|
||||
{isNormal && !isFreePlan && (
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
disabled={isCancelled || !canManagePayment}
|
||||
onClick={() => {
|
||||
if (isCancelled) return;
|
||||
setCancelModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span>{isCancelled ? t('common:cancelled') : t('common:cancel_plan')}</span>
|
||||
</Button>
|
||||
)}
|
||||
{isCancelled && (
|
||||
<Button
|
||||
size="lg"
|
||||
disabled={resumePlanMutation.isLoading || !canManagePayment}
|
||||
onClick={() => {
|
||||
if (!subscription) return;
|
||||
|
||||
const statusLower = subscription.Status?.toLowerCase?.() || '';
|
||||
const isDeleted = statusLower === 'deleted';
|
||||
const periodEndMs = subscription.CurrentPeriodEndAt
|
||||
? new Date(subscription.CurrentPeriodEndAt).getTime()
|
||||
: 0;
|
||||
const isExpired = !periodEndMs || periodEndMs <= Date.now();
|
||||
const statusLower = subscription.Status?.toLowerCase?.() || '';
|
||||
const isDeleted = statusLower === 'deleted';
|
||||
const periodEndMs = subscription.CurrentPeriodEndAt
|
||||
? new Date(subscription.CurrentPeriodEndAt).getTime()
|
||||
: 0;
|
||||
const isExpired = !periodEndMs || periodEndMs <= Date.now();
|
||||
|
||||
if (isDeleted || isExpired) {
|
||||
toast({
|
||||
title: t('common:resume_plan_expired_title'),
|
||||
description: t('common:resume_plan_expired_desc'),
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isDeleted || isExpired) {
|
||||
toast({
|
||||
title: t('common:resume_plan_expired_title'),
|
||||
description: t('common:resume_plan_expired_desc'),
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payMethod =
|
||||
subscription.PayMethod === 'balance' || subscription.PayMethod === 'stripe'
|
||||
? subscription.PayMethod
|
||||
: 'stripe';
|
||||
const payMethod =
|
||||
subscription.PayMethod === 'balance' || subscription.PayMethod === 'stripe'
|
||||
? subscription.PayMethod
|
||||
: 'stripe';
|
||||
|
||||
resumePlanMutation.mutate({
|
||||
workspace: subscription.Workspace,
|
||||
regionDomain: subscription.RegionDomain,
|
||||
planName: subscription.PlanName,
|
||||
payMethod,
|
||||
operator: 'resumed'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>{t('common:renew_plan')}</span>
|
||||
</Button>
|
||||
)}
|
||||
resumePlanMutation.mutate({
|
||||
workspace: subscription.Workspace,
|
||||
regionDomain: subscription.RegionDomain,
|
||||
planName: subscription.PlanName,
|
||||
payMethod,
|
||||
operator: 'resumed'
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Sparkles />
|
||||
<span>{t('common:renew_plan')}</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Keep UpgradePlanDialog mounted even when cancelled, so message-driven open works */}
|
||||
{children?.({
|
||||
trigger: isCancelled ? (
|
||||
<span className="hidden" />
|
||||
) : (
|
||||
<Button size="lg">
|
||||
<Sparkles />
|
||||
<span>{inDebt ? t('common:renew') : t('common:upgrade_plan')}</span>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* Keep UpgradePlanDialog mounted even when cancelled, so message-driven open works */}
|
||||
{children?.({
|
||||
trigger: isCancelled ? (
|
||||
<span className="hidden" />
|
||||
) : (
|
||||
<Button size="lg" disabled={!canManagePayment}>
|
||||
<Sparkles />
|
||||
<span>{inDebt ? t('common:renew') : t('common:upgrade_plan')}</span>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{t('common:can_not_manage_payments')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<Separator className="border-slate-200" />
|
||||
|
||||
@@ -17,6 +17,9 @@ export const OperatorSchema = z.enum([
|
||||
]);
|
||||
export type Operator = z.infer<typeof OperatorSchema>;
|
||||
|
||||
export const WorkspaceRoleSchema = z.enum(['MANAGER', 'DEVELOPER', 'OWNER']);
|
||||
export type WorkspaceRole = z.infer<typeof WorkspaceRoleSchema>;
|
||||
|
||||
// Stripe 信息
|
||||
export const StripeInfoSchema = z.object({
|
||||
subscriptionId: z.string(),
|
||||
@@ -95,7 +98,8 @@ export const WorkspaceSubscriptionSchema = z.object({
|
||||
ExpireAt: z.string().nullable(),
|
||||
Traffic: z.array(z.any()).nullable(),
|
||||
type: SubscriptionTypeSchema,
|
||||
InvoiceInfo: InvoiceInfoSchema.optional()
|
||||
InvoiceInfo: InvoiceInfoSchema.optional(),
|
||||
role: WorkspaceRoleSchema
|
||||
});
|
||||
export type WorkspaceSubscription = z.infer<typeof WorkspaceSubscriptionSchema>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user