From 48c9d985b281e2a6abaa3d6f9458b7a68a33afd2 Mon Sep 17 00:00:00 2001 From: devinxl Date: Thu, 10 Oct 2024 17:19:16 +0800 Subject: [PATCH] feat(dcellar-web-ui): support reactive the frozen account --- apps/dcellar-web-ui/.eslintrc.js | 1 + .../RenewalNotification/RenewalGuideModal.tsx | 73 ++++++ .../components/RenewalNotification/Step.tsx | 27 +++ .../components/RenewalNotification/index.tsx | 224 ++++++++++++++++++ .../common/DiscontinueBanner/index.tsx | 11 +- .../GlobalManagements/AccountsDataLoader.tsx | 9 +- .../src/components/layout/index.tsx | 6 +- apps/dcellar-web-ui/src/facade/account.ts | 2 +- apps/dcellar-web-ui/src/hooks/useBalance.ts | 10 +- .../accounts/components/AccountDetailNav.tsx | 3 +- .../modules/accounts/components/BasicInfo.tsx | 4 +- .../modules/accounts/components/MetaInfo.tsx | 8 +- .../src/modules/accounts/hooks.ts | 10 +- .../src/modules/bucket/index.tsx | 2 + .../src/modules/dashboard/index.tsx | 2 + .../object/components/CreateObject.tsx | 26 +- .../components/DetailObjectOperation.tsx | 6 +- .../object/components/InsufficientBalance.tsx | 50 ++-- .../modules/object/components/ObjectList.tsx | 6 +- .../src/modules/object/index.tsx | 16 +- .../src/modules/wallet/Send/index.tsx | 33 ++- .../dcellar-web-ui/src/pages/wallet/index.tsx | 1 + apps/dcellar-web-ui/src/store/reducers.ts | 9 + .../src/store/slices/accounts.ts | 79 +++++- .../dcellar-web-ui/src/store/slices/global.ts | 6 + .../src/store/slices/session-persist.ts | 23 ++ .../src/utils/payment/index.tsx | 4 +- apps/dcellar-web-ui/src/utils/time.ts | 3 - 28 files changed, 542 insertions(+), 112 deletions(-) create mode 100644 apps/dcellar-web-ui/src/components/RenewalNotification/RenewalGuideModal.tsx create mode 100644 apps/dcellar-web-ui/src/components/RenewalNotification/Step.tsx create mode 100644 apps/dcellar-web-ui/src/components/RenewalNotification/index.tsx create mode 100644 apps/dcellar-web-ui/src/store/slices/session-persist.ts diff --git a/apps/dcellar-web-ui/.eslintrc.js b/apps/dcellar-web-ui/.eslintrc.js index 72b3cf05..b32db358 100644 --- a/apps/dcellar-web-ui/.eslintrc.js +++ b/apps/dcellar-web-ui/.eslintrc.js @@ -24,6 +24,7 @@ module.exports = { rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', + 'react-hooks/exhaustive-deps': 1, }, settings: { react: { diff --git a/apps/dcellar-web-ui/src/components/RenewalNotification/RenewalGuideModal.tsx b/apps/dcellar-web-ui/src/components/RenewalNotification/RenewalGuideModal.tsx new file mode 100644 index 00000000..7396c115 --- /dev/null +++ b/apps/dcellar-web-ui/src/components/RenewalNotification/RenewalGuideModal.tsx @@ -0,0 +1,73 @@ +import { Box, Flex, ModalBody, ModalCloseButton, ModalFooter, Text } from '@node-real/uikit'; +import { DCButton } from '../common/DCButton'; +import { DCModal } from '../common/DCModal'; +import { Step } from '@/components/RenewalNotification/Step'; +import { useRouter } from 'next/router'; +import { InternalRoutePaths } from '@/constants/paths'; + +const STEP_DATA = [ + { + num: 1, + description: 'Transfer in enough BNB to your Owner Account.', + }, + { + num: 2, + description: + 'Deposit BNB from your Owner Account to your Payment Account which shares the same address with your Payment Account.', + }, +]; + +export type RenewalGuideModalProps = { + isOpen: boolean; + onClose: () => void; +}; +export const RenewalGuideModal = ({ isOpen, onClose }: RenewalGuideModalProps) => { + const router = useRouter(); + const onNavigate = (path: string) => { + router.push(path); + }; + return ( + + + + + DCellar Renewal Guide + + + Your Owner Account has been frozen due to insufficient funds. The Payment Account + associated with the same address has also had its bucket restricted in service. Follow the + following steps to unfreeze your account and restore your data service. + + + {STEP_DATA.map((step, index) => { + return ( + <> + + {index !== STEP_DATA.length - 1 && ( + + )} + + ); + })} + + + + { + onNavigate(InternalRoutePaths.transfer_in); + }} + > + Transfer In + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/components/RenewalNotification/Step.tsx b/apps/dcellar-web-ui/src/components/RenewalNotification/Step.tsx new file mode 100644 index 00000000..9ba33633 --- /dev/null +++ b/apps/dcellar-web-ui/src/components/RenewalNotification/Step.tsx @@ -0,0 +1,27 @@ +import { Box, Flex, Text } from '@node-real/uikit'; + +export type StepProps = { + num: number; + description: string; +}; +export const Step = ({ num, description }: StepProps) => { + return ( + + + + + + Step {num} + + {description} + + ); +}; diff --git a/apps/dcellar-web-ui/src/components/RenewalNotification/index.tsx b/apps/dcellar-web-ui/src/components/RenewalNotification/index.tsx new file mode 100644 index 00000000..e75b2af3 --- /dev/null +++ b/apps/dcellar-web-ui/src/components/RenewalNotification/index.tsx @@ -0,0 +1,224 @@ +import { GREENFIELD_CHAIN_ID } from '@/base/env'; +import { IconFont } from '@/components/IconFont'; +import { RenewalGuideModal } from '@/components/RenewalNotification/RenewalGuideModal'; +import { InternalRoutePaths } from '@/constants/paths'; +import { MIN_AMOUNT } from '@/modules/wallet/constants'; +import { useAppDispatch, useAppSelector } from '@/store'; +import { EStreamRecordStatus, selectPaymentAccounts } from '@/store/slices/accounts'; +import { setCloseRenewalAddresses } from '@/store/slices/session-persist'; +import { displayTokenSymbol } from '@/utils/wallet'; +import { Box, Button, Flex, useDisclosure } from '@node-real/uikit'; +import { fetchBalance } from '@wagmi/core'; +import { useAsyncEffect } from 'ahooks'; +import BigNumber from 'bignumber.js'; +import dayjs from 'dayjs'; +import { isEmpty } from 'lodash-es'; +import { useRouter } from 'next/router'; +import { ReactNode, useMemo, useState } from 'react'; + +export type RenewalNotificationProps = { + address?: string; +}; + +export const RenewalNotification = ({ address }: RenewalNotificationProps) => { + const router = useRouter(); + const dispatch = useAppDispatch(); + const { isOpen, onClose, onOpen } = useDisclosure(); + const loginAccount = useAppSelector((root) => root.persist.loginAccount); + const _bankBalance = useAppSelector((root) => root.accounts.bankOrWalletBalance); + const [bankBalance, setBankBalance] = useState(null); + const accountInfos = useAppSelector((root) => root.accounts.accountInfos); + const { reserveTime } = useAppSelector((root) => root.global.storeFeeParams); + const paymentAccountList = useAppSelector(selectPaymentAccounts(loginAccount)); + const closeRenewalAddresses = useAppSelector((root) => root.sessionPersist.closeRenewalAddresses); + + useAsyncEffect(async () => { + if (!loginAccount) return; + const data = await fetchBalance({ + address: loginAccount as `0x${string}`, + chainId: GREENFIELD_CHAIN_ID, + }); + setBankBalance(data.formatted); + }, [loginAccount, _bankBalance]); + + const notifications = useMemo(() => { + const nodes: { type: 'danger' | 'warning'; node: ReactNode }[] = []; + const onNavDeposit = (address: string) => { + const isOwner = address.toLowerCase() === loginAccount.toLowerCase(); + if (isOwner) { + return onOpen(); + } + + return router.push( + `${InternalRoutePaths.wallet}?type=send&from=${loginAccount}&to=${address}`, + ); + }; + const onCloseNotification = (address: string) => { + dispatch(setCloseRenewalAddresses([...closeRenewalAddresses, address])); + }; + + const ownAccounts = [loginAccount, ...paymentAccountList.map((item) => item.address)]; + if ((address && !accountInfos[address]) || bankBalance === null || isEmpty(accountInfos)) { + return nodes; + } + const accounts = (address ? [address] : ownAccounts).filter((item) => !!item) || []; + + for (const _account of accounts) { + const item = accountInfos[_account]; + + // item.id === '' means the account is not belong to loginAccount + if (!item || closeRenewalAddresses.includes(item.address)) { + continue; + } + + if (item.status === EStreamRecordStatus.FROZEN) { + const renewalStoreFee = BigNumber(item.frozenNetflowRate).times(reserveTime).abs(); + const node = ( + + + + + + + Your{' '} + + {item.name} + {' '} + is frozen, associated storage services are currently limited. To avoid data loss, + please deposit at least{' '} + + {renewalStoreFee.isLessThan(MIN_AMOUNT) + ? MIN_AMOUNT + : renewalStoreFee.toFixed(8, 0)}{' '} + {displayTokenSymbol()} + {' '} + to reactive.{' '} + + + + { + onCloseNotification(item.address); + }} + color="readable.secondary" + type="close" + w={'16'} + /> + + ); + nodes.push({ type: 'danger', node }); + continue; + } + + // The purpose of settle is to lock in the costs for the next 6 months. At the time of settleTime, the user's costs from the last settlement (crudtimeStamp) to the current time will be deducted, primarily using the payment's buffer balance. If the storage price changes during the storage period, causing the buffer balance to be insufficient to cover the deduction, the remaining amount will be paid from the static balance. The future 6 months' costs also need to be locked in by transferring from the static balance/bank balance to the buffer balance. + const nextStoreFee = BigNumber(item.netflowRate).times(reserveTime).abs(); + const curExtraFee = BigNumber(item.bufferBalance) + .minus( + BigNumber(item.netflowRate) + .abs() + .times(dayjs().unix() - item.crudTimestamp), + ) + .isPositive() + ? 0 + : BigNumber(item.bufferBalance) + .minus( + BigNumber(item.netflowRate) + .abs() + .times(dayjs().unix() - item.crudTimestamp), + ) + .abs(); + const fee = nextStoreFee.plus(curExtraFee); + const isOwnerAccount = item.address.toLowerCase() === loginAccount.toLowerCase(); + const lessThan7Days = + item.settleTimestamp !== 0 ? item.settleTimestamp - dayjs().unix() < 1 * 60 : false; + const notPayNextFee = isOwnerAccount + ? BigNumber(item.staticBalance).plus(bankBalance).isLessThan(fee) + : BigNumber(item.staticBalance).isLessThan(fee); + + if (lessThan7Days && notPayNextFee) { + const node = ( + + + + + + + Your{' '} + + {item.name} + {' '} + is estimated to settle on {dayjs(item.settleTimestamp * 1000).format('MMM-DD-YYYY')} + . To avoid account freezing and potential data loss, please deposit at least{' '} + + {fee.isLessThan(MIN_AMOUNT) ? MIN_AMOUNT : fee.toFixed(8, 0)}{' '} + {displayTokenSymbol()} + {' '} + into your payment account or associated owner account.{' '} + + + + { + onCloseNotification(item.address); + }} + color="readable.secondary" + type="close" + w={'16'} + /> + + ); + nodes.push({ type: 'warning', node }); + } + } + return nodes; + }, [ + loginAccount, + paymentAccountList, + address, + accountInfos, + bankBalance, + router, + onOpen, + dispatch, + closeRenewalAddresses, + reserveTime, + ]); + + return ( + <> + + {notifications.map((item, index) => ( + + {item.node} + + ))} + + + + ); +}; diff --git a/apps/dcellar-web-ui/src/components/common/DiscontinueBanner/index.tsx b/apps/dcellar-web-ui/src/components/common/DiscontinueBanner/index.tsx index 6db7fe61..6f4d95ef 100644 --- a/apps/dcellar-web-ui/src/components/common/DiscontinueBanner/index.tsx +++ b/apps/dcellar-web-ui/src/components/common/DiscontinueBanner/index.tsx @@ -26,16 +26,11 @@ export const DiscontinueBanner = ({ borderRadius={'4px'} background={bg} color={color} - p={8} + p={'8px 12px'} + gap={8} > {icon} - + {content} diff --git a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/AccountsDataLoader.tsx b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/AccountsDataLoader.tsx index 363d7104..aba882cc 100644 --- a/apps/dcellar-web-ui/src/components/layout/GlobalManagements/AccountsDataLoader.tsx +++ b/apps/dcellar-web-ui/src/components/layout/GlobalManagements/AccountsDataLoader.tsx @@ -18,7 +18,7 @@ export const AccountsDataLoader = () => { const currentBucketName = useAppSelector((root) => root.object.currentBucketName); const { asPath } = useRouter(); - const { data: gnfdBalance, refetch } = useBalance({ + const { data: gnfdBalance } = useBalance({ address: loginAccount as any, chainId: GREENFIELD_CHAIN_ID, }); @@ -31,13 +31,6 @@ export const AccountsDataLoader = () => { dispatch(setupPaymentAccounts()); }, [dispatch, loginAccount]); - useAsyncEffect(async () => { - if (!loginAccount) return; - // update metamask - refetch(); - dispatch(setBankOrWalletBalance(metamaskValue)); - }, [asPath, refetch, loginAccount]); - useThrottleEffect(() => { dispatch(setBankOrWalletBalance(metamaskValue)); }, [metamaskValue]); diff --git a/apps/dcellar-web-ui/src/components/layout/index.tsx b/apps/dcellar-web-ui/src/components/layout/index.tsx index 99261e2e..771f3e79 100644 --- a/apps/dcellar-web-ui/src/components/layout/index.tsx +++ b/apps/dcellar-web-ui/src/components/layout/index.tsx @@ -2,7 +2,7 @@ import { IconFont } from '@/components/IconFont'; import { Header } from '@/components/layout/Header'; import { Nav } from '@/components/layout/Nav'; import { useAppDispatch, useAppSelector } from '@/store'; -import { selectAccount } from '@/store/slices/accounts'; +import { EStreamRecordStatus, selectAccount } from '@/store/slices/accounts'; import { setObjectOperation } from '@/store/slices/object'; import styled from '@emotion/styled'; import { Flex, Grid } from '@node-real/uikit'; @@ -54,7 +54,7 @@ export const Layout = memo(function Layout({ children }) { isBucketDiscontinue || isBucketMigrating || !isBucketOwner || - accountDetail.clientFrozen || + accountDetail.status === EStreamRecordStatus.FROZEN || !folderExist ) return; @@ -103,7 +103,7 @@ const Notification = styled(Flex)` gap: 8px; border-radius: 4px; background-color: rgba(238, 57, 17, 0.1); - padding: 13px 20px; + padding: 8px 12px; margin-bottom: 16px; `; diff --git a/apps/dcellar-web-ui/src/facade/account.ts b/apps/dcellar-web-ui/src/facade/account.ts index a4a961ab..32312d74 100644 --- a/apps/dcellar-web-ui/src/facade/account.ts +++ b/apps/dcellar-web-ui/src/facade/account.ts @@ -238,7 +238,7 @@ export const getPaymentAccountsByOwner = async ( .then(resolve, commonFault); }; -export const sendToOwnerAccount = async ( +export const sendToExternalAccount = async ( { fromAddress, toAddress, amount }: { fromAddress: string; toAddress: string; amount: string }, connector: Connector, ): Promise => { diff --git a/apps/dcellar-web-ui/src/hooks/useBalance.ts b/apps/dcellar-web-ui/src/hooks/useBalance.ts index b007f01e..baad39ae 100644 --- a/apps/dcellar-web-ui/src/hooks/useBalance.ts +++ b/apps/dcellar-web-ui/src/hooks/useBalance.ts @@ -17,12 +17,13 @@ export const useBalance = ({ const [balance, setBalance] = useState({} as FetchBalanceResult); const [timers, setTimers] = useState({}); - const refetch = useCallback(() => { + const refetch = useCallback(async () => { if (!address || !chainId) return Promise.resolve({} as FetchBalanceResult); - return fetchBalance({ + const data = await fetchBalance({ address: address, chainId: chainId, }); + setBalance(data); }, [address, chainId]); useEffect(() => { @@ -32,15 +33,14 @@ export const useBalance = ({ if (timer) return; Object.values(timers).forEach((timer) => timer && clearIntervalAsync(timer)); timers[key] = setIntervalAsync(async () => { - const data = await refetch(); - setBalance(data); + await refetch(); }, intervalMs); setTimers(timers); return () => { Object.values(timers).forEach((timer) => timer && clearIntervalAsync(timer)); }; - }, [address, chainId]); + }, [address, chainId, intervalMs, refetch, timers]); return { data: balance, diff --git a/apps/dcellar-web-ui/src/modules/accounts/components/AccountDetailNav.tsx b/apps/dcellar-web-ui/src/modules/accounts/components/AccountDetailNav.tsx index 3dde03a2..272726f7 100644 --- a/apps/dcellar-web-ui/src/modules/accounts/components/AccountDetailNav.tsx +++ b/apps/dcellar-web-ui/src/modules/accounts/components/AccountDetailNav.tsx @@ -2,6 +2,7 @@ import { IconFont } from '@/components/IconFont'; import { Tips } from '@/components/common/Tips'; import { InternalRoutePaths } from '@/constants/paths'; import { useAppSelector } from '@/store'; +import { EStreamRecordStatus } from '@/store/slices/accounts'; import { selectStoreFeeParams } from '@/store/slices/global'; import { BN } from '@/utils/math'; import { displayTokenSymbol } from '@/utils/wallet'; @@ -19,7 +20,7 @@ export const AccountDetailNav = ({ address }: { address: string }) => { const curAddress = address as string; const isOwnerAccount = address === loginAccount; const accountDetail = accountInfos?.[curAddress] || {}; - const isFrozen = accountDetail.clientFrozen; + const isFrozen = accountDetail.status === EStreamRecordStatus.FROZEN; const loading = !address || isEmpty(accountDetail); const unFreezeAmount = useMemo(() => { diff --git a/apps/dcellar-web-ui/src/modules/accounts/components/BasicInfo.tsx b/apps/dcellar-web-ui/src/modules/accounts/components/BasicInfo.tsx index 9c0efa8b..c64da04a 100644 --- a/apps/dcellar-web-ui/src/modules/accounts/components/BasicInfo.tsx +++ b/apps/dcellar-web-ui/src/modules/accounts/components/BasicInfo.tsx @@ -9,7 +9,7 @@ import { FULL_DISPLAY_PRECISION, } from '@/modules/wallet/constants'; import { useAppSelector } from '@/store'; -import { AccountInfo } from '@/store/slices/accounts'; +import { AccountInfo, EStreamRecordStatus } from '@/store/slices/accounts'; import { selectBnbUsdtExchangeRate, selectStoreFeeParams } from '@/store/slices/global'; import { currencyFormatter } from '@/utils/formatter'; import { BN } from '@/utils/math'; @@ -42,7 +42,7 @@ export const BasicInfo = ({ loading, title, accountDetail, availableBalance }: P .plus(BN(bankBalance)) .toString(DECIMAL_NUMBER) : BN(accountDetail?.staticBalance || 0).toString(DECIMAL_NUMBER); - const isFrozen = accountDetail?.clientFrozen; + const isFrozen = accountDetail.status === EStreamRecordStatus.FROZEN; const unFreezeAmount = useMemo(() => { return BN(storeFeeParams.reserveTime).times(BN(accountDetail?.frozenNetflowRate)).toString(); diff --git a/apps/dcellar-web-ui/src/modules/accounts/components/MetaInfo.tsx b/apps/dcellar-web-ui/src/modules/accounts/components/MetaInfo.tsx index ee5db31e..9116c008 100644 --- a/apps/dcellar-web-ui/src/modules/accounts/components/MetaInfo.tsx +++ b/apps/dcellar-web-ui/src/modules/accounts/components/MetaInfo.tsx @@ -8,7 +8,11 @@ import { FULL_DISPLAY_PRECISION, } from '@/modules/wallet/constants'; import { useAppDispatch, useAppSelector } from '@/store'; -import { selectAccount, setEditingPaymentAccountRefundable } from '@/store/slices/accounts'; +import { + EStreamRecordStatus, + selectAccount, + setEditingPaymentAccountRefundable, +} from '@/store/slices/accounts'; import { selectBnbUsdtExchangeRate } from '@/store/slices/global'; import { currencyFormatter } from '@/utils/formatter'; import { BN } from '@/utils/math'; @@ -36,7 +40,7 @@ export const MetaInfo = memo(function MetaInfo({ address }: Props) { const loading = !address || isEmpty(accountDetail); const availableBalance = isOwnerAccount ? bankBalance : accountDetail.staticBalance; const isRefundable = accountDetail.refundable; - const isFrozen = accountDetail.clientFrozen; + const isFrozen = accountDetail.status === EStreamRecordStatus.FROZEN; const onAction = (e: string) => { if (e === 'withdraw') { diff --git a/apps/dcellar-web-ui/src/modules/accounts/hooks.ts b/apps/dcellar-web-ui/src/modules/accounts/hooks.ts index 62f1c400..d307ee0a 100644 --- a/apps/dcellar-web-ui/src/modules/accounts/hooks.ts +++ b/apps/dcellar-web-ui/src/modules/accounts/hooks.ts @@ -5,7 +5,7 @@ import { BN } from '@/utils/math'; import { getUtcDayjs } from '@/utils/time'; import { isEmpty } from 'lodash-es'; import { useMemo } from 'react'; -import { CRYPTOCURRENCY_DISPLAY_PRECISION } from '../wallet/constants'; +import { CRYPTOCURRENCY_DISPLAY_PRECISION, MIN_AMOUNT } from '../wallet/constants'; export const useUnFreezeAmount = (address: string) => { const storeFeeParams = useAppSelector(selectStoreFeeParams); @@ -13,9 +13,11 @@ export const useUnFreezeAmount = (address: string) => { if (isEmpty(storeFeeParams) || isEmpty(account)) return '--'; - return BN(storeFeeParams.reserveTime) - .times(BN(account.frozenNetflowRate || account.netflowRate)) - .toString(); + return BN(storeFeeParams.reserveTime).times(BN(account.frozenNetflowRate)).isLessThan(MIN_AMOUNT) + ? MIN_AMOUNT + : BN(storeFeeParams.reserveTime) + .times(BN(account.frozenNetflowRate || account.netflowRate)) + .toFixed(8, 0); }; type EstimateCostType = 'cur' | 'next'; diff --git a/apps/dcellar-web-ui/src/modules/bucket/index.tsx b/apps/dcellar-web-ui/src/modules/bucket/index.tsx index 59d4dd99..d034b925 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/index.tsx +++ b/apps/dcellar-web-ui/src/modules/bucket/index.tsx @@ -7,6 +7,7 @@ import { PageTitle } from '@/components/layout/PageTitle'; import { DiscontinueBanner } from '@/components/common/DiscontinueBanner'; import { BucketOperations } from '@/modules/bucket/components/BucketOperations'; import { GAContextProvider } from '@/context/GAContext'; +import { RenewalNotification } from '@/components/RenewalNotification'; export const BucketPage = () => { const dispatch = useAppDispatch(); @@ -32,6 +33,7 @@ export const BucketPage = () => { + {hasDiscontinueBucket && ( { const dispatch = useAppDispatch(); @@ -38,6 +39,7 @@ export const Dashboard = () => { return ( + Dashboard diff --git a/apps/dcellar-web-ui/src/modules/object/components/CreateObject.tsx b/apps/dcellar-web-ui/src/modules/object/components/CreateObject.tsx index 651c2d49..3e5f8a97 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/CreateObject.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/CreateObject.tsx @@ -5,7 +5,7 @@ import { useHandleFolderTree } from '@/hooks/useHandleFolderTree'; import { BatchOperations } from '@/modules/object/components/BatchOperations'; import { UploadMenuList } from '@/modules/object/components/UploadMenuList'; import { useAppDispatch, useAppSelector } from '@/store'; -import { selectAccount } from '@/store/slices/accounts'; +import { EStreamRecordStatus, selectAccount } from '@/store/slices/accounts'; import { setupBucket, setupBucketQuota } from '@/store/slices/bucket'; import { SELECT_OBJECT_NUM_LIMIT, @@ -111,7 +111,7 @@ export const CreateObject = memo(function NewObject({ isFlowRateLimit || isBucketMigrating || loading || - accountDetail.clientFrozen; + accountDetail.status === EStreamRecordStatus.FROZEN; const uploadDisabled = isBucketDiscontinue || isFlowRateLimit || @@ -119,7 +119,7 @@ export const CreateObject = memo(function NewObject({ invalidPath || pathSegments.length > MAX_FOLDER_LEVEL || loading || - accountDetail.clientFrozen; + accountDetail.status === EStreamRecordStatus.FROZEN; const onFilesChange = async (e: ChangeEvent) => { const files = e.target.files; @@ -197,16 +197,16 @@ export const CreateObject = memo(function NewObject({ isBucketDiscontinue ? 'Bucket in the discontinue status cannot upload objects.' : isFlowRateLimit - ? "The bucket's flow rate exceeds the payment account limit. Contact the account owner or switch accounts to increase it." - : isBucketMigrating - ? 'Bucket in the migrating status cannot upload objects.' - : accountDetail?.clientFrozen - ? 'The payment account in the frozen status cannot upload objects.' - : uploadDisabled - ? 'Path invalid' - : `Please limit object size to ${formatBytes( - SINGLE_OBJECT_MAX_SIZE, - )} and upload a maximum of ${SELECT_OBJECT_NUM_LIMIT} objects at a time.` + ? "The bucket's flow rate exceeds the payment account limit. Contact the account owner or switch accounts to increase it." + : isBucketMigrating + ? 'Bucket in the migrating status cannot upload objects.' + : accountDetail.status === EStreamRecordStatus.FROZEN + ? 'The payment account in the frozen status cannot upload objects.' + : uploadDisabled + ? 'Path invalid' + : `Please limit object size to ${formatBytes( + SINGLE_OBJECT_MAX_SIZE, + )} and upload a maximum of ${SELECT_OBJECT_NUM_LIMIT} objects at a time.` } >
diff --git a/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx b/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx index ee05d0ea..84e0c9f5 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/DetailObjectOperation.tsx @@ -13,7 +13,7 @@ import { } from '@/modules/object/components/renderRows'; import { EMPTY_TX_HASH } from '@/modules/object/constant'; import { useAppDispatch, useAppSelector } from '@/store'; -import { AccountInfo } from '@/store/slices/accounts'; +import { AccountInfo, EStreamRecordStatus } from '@/store/slices/accounts'; import { TBucket, setBucketQuota } from '@/store/slices/bucket'; import { ObjectActionType, @@ -274,7 +274,7 @@ export const DetailObjectOperation = memo( variant="ghost" flex={1} gaClickName="dc.file.f_detail_pop.share.click" - isDisabled={bucketAccountDetail.clientFrozen} + isDisabled={bucketAccountDetail.status === EStreamRecordStatus.FROZEN} onClick={() => onAction('view')} > Preview @@ -283,7 +283,7 @@ export const DetailObjectOperation = memo( size={'lg'} flex={1} gaClickName="dc.file.f_detail_pop.download.click" - isDisabled={bucketAccountDetail.clientFrozen} + isDisabled={bucketAccountDetail.status === EStreamRecordStatus.FROZEN} onClick={() => onAction('download')} > Download diff --git a/apps/dcellar-web-ui/src/modules/object/components/InsufficientBalance.tsx b/apps/dcellar-web-ui/src/modules/object/components/InsufficientBalance.tsx index fdebd517..e1aa2f22 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/InsufficientBalance.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/InsufficientBalance.tsx @@ -1,51 +1,33 @@ -import { InternalRoutePaths } from '@/constants/paths'; import { useUnFreezeAmount } from '@/modules/accounts/hooks'; import { useAppSelector } from '@/store'; -import { selectAccount } from '@/store/slices/accounts'; +import { EStreamRecordStatus, selectAccount } from '@/store/slices/accounts'; import { selectLocateBucket } from '@/store/slices/object'; import { displayTokenSymbol } from '@/utils/wallet'; import { ColoredWarningIcon } from '@node-real/icons'; -import { Flex, Link } from '@node-real/uikit'; -import { useRouter } from 'next/router'; +import { Box, Flex } from '@node-real/uikit'; export const InsufficientBalance = () => { - const loginAccount = useAppSelector((root) => root.persist.loginAccount); - - const router = useRouter(); const bucket = useAppSelector(selectLocateBucket); const accountDetail = useAppSelector(selectAccount(bucket.PaymentAddress)); const amount = useUnFreezeAmount(bucket.PaymentAddress); - - const isOwnerAccount = bucket.PaymentAddress === loginAccount; - const isFrozen = accountDetail?.clientFrozen; - - const onTopUpClick = () => { - const topUpUrl = isOwnerAccount - ? InternalRoutePaths.transfer_in - : `${InternalRoutePaths.send}&from=${loginAccount}&to=${bucket.PaymentAddress}&amount=${amount}`; - router.push(topUpUrl); - }; + const isFrozen = accountDetail?.status === EStreamRecordStatus.FROZEN; return ( <> {isFrozen && ( - - - Insufficient Balance. Please deposit at least  {amount} {' '} - {displayTokenSymbol()} to renew your service, or your objects may be permanently - deleted.  - { - onTopUpClick(); - }} - > - Top Up - + + + + + + This Bucket's Payment Account is frozen. Currently, all services are restricted. To + prevent data loss, please contact the owner of the associated Payment Account and + deposit at least{' '} + + {amount} {displayTokenSymbol()} + {' '} + to reactivate it. + )} diff --git a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx index 5c0f63b1..4b8c508a 100644 --- a/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx +++ b/apps/dcellar-web-ui/src/modules/object/components/ObjectList.tsx @@ -23,7 +23,7 @@ import { OBJECT_SEALED_STATUS } from '@/modules/object/constant'; import { StyledRow } from '@/modules/object/objects.style'; import { contentTypeToExtension } from '@/modules/object/utils'; import { useAppDispatch, useAppSelector } from '@/store'; -import { selectAccount } from '@/store/slices/accounts'; +import { EStreamRecordStatus, selectAccount } from '@/store/slices/accounts'; import { setBucketQuota, setupBucketQuota } from '@/store/slices/bucket'; import { setSignatureAction } from '@/store/slices/global'; import { @@ -257,7 +257,7 @@ export const ObjectList = memo(function ObjectList({ shareMode ), @@ -326,7 +326,7 @@ export const ObjectList = memo(function ObjectList({ shareMode const isSealed = record.objectStatus === OBJECT_SEALED_STATUS; // if account frozen, disabled 'download' & 'delete' - if (accountDetail?.clientFrozen) { + if (accountDetail.status === EStreamRecordStatus.FROZEN) { pruneActions = pickAction(pruneActions, ['delete', 'download']); } diff --git a/apps/dcellar-web-ui/src/modules/object/index.tsx b/apps/dcellar-web-ui/src/modules/object/index.tsx index 1600c63c..19a341f4 100644 --- a/apps/dcellar-web-ui/src/modules/object/index.tsx +++ b/apps/dcellar-web-ui/src/modules/object/index.tsx @@ -23,7 +23,7 @@ import { SelectedText, } from '@/modules/object/objects.style'; import { useAppDispatch, useAppSelector } from '@/store'; -import { setupAccountRecords } from '@/store/slices/accounts'; +import { setupAccountRecords, selectPaymentAccounts } from '@/store/slices/accounts'; import { setBucketStatus, setupBucket } from '@/store/slices/bucket'; import { setPathSegments } from '@/store/slices/object'; import { setPrimarySpInfo, SpEntity } from '@/store/slices/sp'; @@ -32,6 +32,7 @@ import { ObjectOperations } from '@/modules/object/components/ObjectOperations'; import { BucketStatus as BucketStatusEnum } from '@bnb-chain/greenfield-js-sdk'; import { DiscontinueBanner } from '@/components/common/DiscontinueBanner'; import { MigratingBucketNoticeBanner } from './components/MigratingBucketNoticeBanner'; +import { RenewalNotification } from '@/components/RenewalNotification'; export const ObjectsPage = () => { const dispatch = useAppDispatch(); @@ -42,15 +43,16 @@ export const ObjectsPage = () => { const objectSelectedKeys = useAppSelector((root) => root.object.objectSelectedKeys); const isBucketDiscontinue = useAppSelector((root) => root.bucket.isBucketDiscontinue); const isBucketMigrating = useAppSelector((root) => root.bucket.isBucketMigrating); + const paymentAccountList = useAppSelector(selectPaymentAccounts(loginAccount)); const allSpList = useAppSelector((root) => root.sp.allSpList); - + const ownAccounts = [loginAccount, ...paymentAccountList.map((item) => item.address)]; const { path } = router.query; const items = path as string[]; const title = last(items)!; const [bucketName, ...folders] = items; const bucket = bucketRecords[bucketName]; const isFlowRateLimit = ['1', '3'].includes(bucket?.OffChainStatus); - + const isOwnAccount = ownAccounts.includes(bucket?.PaymentAddress); const selected = objectSelectedKeys.length; const goBack = () => { @@ -131,7 +133,13 @@ export const ObjectsPage = () => { {isBucketOwner ? ( - + <> + {isOwnAccount ? ( + + ) : ( + + )} + ) : ( (function Send() { }; const txType = useMemo(() => { - if (isEmpty(transferToAccount) || isEmpty(transferFromAccount)) return; + if (isEmpty(transferToAccount) || isEmpty(transferFromAccount) || isEmpty(loginAccount)) return; + if ( + transferFromAccount.address.toLowerCase() === loginAccount.toLowerCase() && + transferToAccount.address.toLowerCase() === loginAccount.toLowerCase() + ) { + return 'deposit_to_owner_account'; + } if ( transferFromAccount.name.toLowerCase() === 'owner account' && ['payment_account', 'non_refundable_payment_account'].includes( accountTypeRecords[transferToAccount.address], ) ) { - return 'send_to_payment_account'; + return 'deposit_to_payment_account'; } if (transferFromAccount.name.toLowerCase().includes('payment account')) { return 'withdraw_from_payment_account'; @@ -156,10 +164,11 @@ export const Send = memo(function Send() { transferFromAccount.name.toLowerCase() === 'owner account' && ['gnfd_account', 'unknown_account'].includes(accountTypeRecords[transferToAccount.address]) ) { - return 'send_to_owner_account'; + return 'send_to_external_account'; } - }, [accountTypeRecords, transferFromAccount, transferToAccount]); + }, [accountTypeRecords, loginAccount, transferFromAccount, transferToAccount]); + console.log('txType', txType); const txCallback = ({ res, error, @@ -201,6 +210,7 @@ export const Send = memo(function Send() { if (!connector) return; if ( txType !== 'withdraw_from_payment_account' && + txType !== 'deposit_to_owner_account' && transferFromAccount.address === transferToAccount.address ) { return toast.error({ @@ -209,7 +219,8 @@ export const Send = memo(function Send() { }); } switch (txType) { - case 'send_to_payment_account': { + case 'deposit_to_owner_account': + case 'deposit_to_payment_account': { onOpen(); const [pRes, pError] = await depositToPaymentAccount( { @@ -235,9 +246,9 @@ export const Send = memo(function Send() { txCallback({ res: wRes, error: wError, freshAddress: [transferFromAccount.address] }); break; } - case 'send_to_owner_account': { + case 'send_to_external_account': { onOpen(); - const [sRes, sError] = await sendToOwnerAccount( + const [sRes, sError] = await sendToExternalAccount( { fromAddress: transferFromAccount.address, toAddress: transferToAccount.address, @@ -300,7 +311,7 @@ export const Send = memo(function Send() { if (!isPaymentAccount) { return errors; } - if (fromAccountDetail?.clientFrozen) { + if (fromAccountDetail?.status === EStreamRecordStatus.FROZEN) { errors.push('This account is frozen due to insufficient balance.'); } if (fromAccountDetail.refundable === false) { diff --git a/apps/dcellar-web-ui/src/pages/wallet/index.tsx b/apps/dcellar-web-ui/src/pages/wallet/index.tsx index a1da6b79..aa6efaa0 100644 --- a/apps/dcellar-web-ui/src/pages/wallet/index.tsx +++ b/apps/dcellar-web-ui/src/pages/wallet/index.tsx @@ -23,6 +23,7 @@ const WalletPage = () => { const { type = EOperation.transfer_in, from, to, amount = '' } = query; const str = Array().concat(type)[0]; const _type = isTransferOperation(str) ? EOperation[str] : EOperation.transfer_in; + console.log('amount', amount); dispatch(setTransferType(_type)); dispatch(setTransferToAddress(to as string)); dispatch(setTransferFromAddress(from as string)); diff --git a/apps/dcellar-web-ui/src/store/reducers.ts b/apps/dcellar-web-ui/src/store/reducers.ts index 267d223c..42e78c40 100644 --- a/apps/dcellar-web-ui/src/store/reducers.ts +++ b/apps/dcellar-web-ui/src/store/reducers.ts @@ -3,6 +3,7 @@ import { HYDRATE } from 'next-redux-wrapper'; import { persistReducer } from 'redux-persist'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; import storage from 'redux-persist/lib/storage'; +import sessionStorage from 'redux-persist/lib/storage/session'; import { runtimeEnv } from '@/base/env'; import accounts from '@/store/slices/accounts'; @@ -16,10 +17,18 @@ import object from '@/store/slices/object'; import persist from '@/store/slices/persist'; import sp from '@/store/slices/sp'; import wallet from '@/store/slices/wallet'; +import sessionPersist from '@/store/slices/session-persist'; + +export const sessionPersistConfig = { + key: 'SESSION_1', + storage: sessionStorage, + whilelist: ['sessionPersist'], +}; const rootReducer = combineReducers({ global, persist, + sessionPersist: persistReducer(sessionPersistConfig, sessionPersist), sp, bucket, wallet, diff --git a/apps/dcellar-web-ui/src/store/slices/accounts.ts b/apps/dcellar-web-ui/src/store/slices/accounts.ts index 6fdcb238..148628a0 100644 --- a/apps/dcellar-web-ui/src/store/slices/accounts.ts +++ b/apps/dcellar-web-ui/src/store/slices/accounts.ts @@ -2,7 +2,7 @@ import { StreamRecord as ChainStreamRecord } from '@bnb-chain/greenfield-cosmos- import { StreamRecord as SpStreamRecord } from '@bnb-chain/greenfield-js-sdk/dist/esm/types/sp/Common'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import BigNumber from 'bignumber.js'; -import { isEmpty, keyBy } from 'lodash-es'; +import { add, capitalize, isEmpty, keyBy } from 'lodash-es'; import { getSpOffChainData } from './persist'; import { AppDispatch, AppState, GetState } from '..'; import { OWNER_ACCOUNT_NAME } from '@/constants/wallet'; @@ -34,6 +34,11 @@ export type AccountType = export type AccountOperationsType = 'oaDetail' | 'paDetail' | 'paCreate' | ''; +export enum EStreamRecordStatus { + 'ACTIVE' = 0, + 'FROZEN' = 1, +} + export type AccountInfo = AccountEntity & { name: string; address: string; @@ -159,7 +164,7 @@ export const paymentAccountSlice = createSlice({ outFlowCount: Number(streamRecord.OutFlowCount), settleTimestamp: Number(streamRecord.SettleTimestamp), clientFrozen: getClientFrozen(+streamRecord.SettleTimestamp, +bufferTime), - frozenNetflowRate: streamRecord.FrozenNetflowRate, + frozenNetflowRate: BigNumber(streamRecord.FrozenNetflowRate).div(1e18).toString(), refundable: item.refundable, status: Number(streamRecord.Status), }; @@ -177,7 +182,7 @@ export const paymentAccountSlice = createSlice({ outFlowCount: Number(streamRecord.outFlowCount?.low), settleTimestamp: Number(streamRecord.settleTimestamp?.low), clientFrozen: getClientFrozen(+streamRecord.settleTimestamp?.low, +bufferTime), - frozenNetflowRate: streamRecord.frozenNetflowRate, + frozenNetflowRate: BigNumber(streamRecord.frozenNetflowRate).div(1e18).toString(), refundable: item.refundable, status: streamRecord.status, }; @@ -185,6 +190,7 @@ export const paymentAccountSlice = createSlice({ }); state.accountInfos = data; }, + setEditingPaymentAccountRefundable: (state, { payload }: PayloadAction) => { state.editingPaymentAccountRefundable = payload; }, @@ -274,12 +280,71 @@ export const setupPaymentAccounts = const [data, error] = await getPaymentAccountsByOwner(loginAccount); const { seedString } = await dispatch(getSpOffChainData(loginAccount, specifiedSp)); + // const testAccounts = [ + // //frozen owner account + // '0x6A69FAA1BD7D73D25A6A3EE1AE41A899DD8CCB8C', + // '0xCDB16F541E1445150F9211DD564668EB01B26E75', + // '0x40EDE296E01E1D57B25697B07D0F1C69077843D0', + // '0xCEE3823C39FCC9845D7C7144A836562F37995085', + // '0x1C893441AB6C1A75E01887087EA508BE8E07AAAE', + // // settime less than 7 days + // '0x367A4BD1606E97647F60DD15FECDCE4535B688F6', + // '0xB4ADFF34EF2C22A4B2FCAA7B955B9FB7BE414E6D', + // '0x78CFE6BCA29CEA13A6C3744D8B6AE86FB576940C', + // '0x9AEAC93ED1444D9E82E2C15F0FD42B0D791A3156', + // '0x3C1A11C54142C44E71A8302AD93AD0191FF17981', + // // // payment accounts + // // '0x4528E40060A22F347EA3BC7EDE62CEA29B5DD837', + // // '0x1745DEB31E405C4CB2C6747E2CFCECA6E57FF77A', + // // '0x258FC67F494A7F25692D02D918E99FA9B29FAEF3', + // // '0x48054722312D664E4C0ADE7FC0C5BD56701BA7D4', + // // '0xF00213234839FE91567E4FFE696A05A078CCF215', + // // '0xFFE7F0C98BB452CD4FA56E9FBE869E502E7186D4', + // // '0x08A8AF3666B39B35C429D8EBA2099B84B999160F', + // // '0xE9AB711EDBBCA0605D7E78E92B14C9D95A0E9D9F', + // // '0xC8B680FB0D2E5B4BEF28195D0D1EE070E271CD84' + // ]; const [paDetail, paError] = await listUserPaymentAccounts( { account: loginAccount }, { type: 'EDDSA', address: loginAccount, domain: window.location.origin, seed: seedString }, { endpoint: spRecords[specifiedSp].endpoint }, ); - + // const customDetail = await Promise.all(testAccounts.map(async (address) => { + // const [res, err] = await getStreamRecord(address); + // const [res2, err2] = await getAccount(address); + // console.log('stream_record', res?.streamRecord); + // console.log('account', res2); + // const StreamRecord = {}; + // // const mapObj = { + // // account: 'Account', + // // bufferBalance: 'BufferBalance', + // // crudtimestamp: 'CrudTimestamp', + // // frozennetflowrate: 'FrozenNetflowRate', + // // lockbalance: 'LockBalance', + // // netflowrate: 'NetflowRate', + // // outflowcount: 'OutflowCount', + // // settletimestamp: 'SettleTimestamp', + // // staticbalance: 'StaticBalance', + // // status: 'Status', + // // } + // const capitalizeFirstLetter = (str) => { + // return str.charAt(0).toUpperCase() + str.slice(1); + // } + // Object.entries(res?.streamRecord || {}).map(([key, value]) => { + // StreamRecord[capitalizeFirstLetter(key)] = value.low ? value.low : value; + // }); + // const PaymentAccount = {}; + // Object.entries(res2 || {}).map(([key, value]) => { + // PaymentAccount[capitalizeFirstLetter(key)] = value + // }); + // return { + // StreamRecord, + // PaymentAccount + // } + // } + // )) + // console.log('customDetail', customDetail); + // console.log('paDetail', paDetail); if (error || paError || paDetail?.code !== 0) { dispatch(setPaymentAccountsLoading(false)); dispatch(setPaymentAccountList({ loginAccount, paymentAccounts: [] })); @@ -298,7 +363,13 @@ export const setupPaymentAccounts = paDetail?.body?.GfSpListUserPaymentAccountsResponse.PaymentAccounts, (item) => item.PaymentAccount.Address, ); + // const keyCustomDetail = keyBy( + // customDetail, + // (item) => item.StreamRecord.Account?.toLowerCase(), + // ); + console.log('keyAccountDetail', keyAccountDetail); + // console.log('keyCustomDetail', keyCustomDetail); let totalPaymentAccountNetflowRate = BN(0); const newPaymentAccounts = data.paymentAccounts.map((address, index) => { const detail = keyAccountDetail[address]; diff --git a/apps/dcellar-web-ui/src/store/slices/global.ts b/apps/dcellar-web-ui/src/store/slices/global.ts index 2526221a..c6287c11 100644 --- a/apps/dcellar-web-ui/src/store/slices/global.ts +++ b/apps/dcellar-web-ui/src/store/slices/global.ts @@ -123,6 +123,7 @@ export interface GlobalState { walletDisconnected: boolean; walletConnected: boolean; signatureAction: SignatureAction | object; + closeRenewalAddresses: string[]; } const defaultStoreFeeParams: StoreFeeParams = { @@ -154,6 +155,7 @@ const initialState: GlobalState = { walletDisconnected: false, walletConnected: false, signatureAction: {}, + closeRenewalAddresses: [], }; export const globalSlice = createSlice({ @@ -443,6 +445,9 @@ export const globalSlice = createSlice({ (task: UploadObject) => !ids.includes(task.id), ); }, + setCloseRenewalAddresses(state, { payload }: PayloadAction) { + state.closeRenewalAddresses = payload; + }, }, }); @@ -469,6 +474,7 @@ export const { setSignatureAction, setWaitQueue, clearUploadRecords, + setCloseRenewalAddresses, } = globalSlice.actions; const _emptyUploadQueue = Array(); diff --git a/apps/dcellar-web-ui/src/store/slices/session-persist.ts b/apps/dcellar-web-ui/src/store/slices/session-persist.ts new file mode 100644 index 00000000..6625d5b1 --- /dev/null +++ b/apps/dcellar-web-ui/src/store/slices/session-persist.ts @@ -0,0 +1,23 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface SessionPersistState { + closeRenewalAddresses: string[]; +} + +const initialState: SessionPersistState = { + closeRenewalAddresses: [], +}; + +export const sessionPersistSlice = createSlice({ + name: 'sessionPersist', + initialState, + reducers: { + setCloseRenewalAddresses(state, { payload }: PayloadAction) { + state.closeRenewalAddresses = payload; + }, + }, +}); + +export const { setCloseRenewalAddresses } = sessionPersistSlice.actions; + +export default sessionPersistSlice.reducer; diff --git a/apps/dcellar-web-ui/src/utils/payment/index.tsx b/apps/dcellar-web-ui/src/utils/payment/index.tsx index 520ba93e..9fb5e518 100644 --- a/apps/dcellar-web-ui/src/utils/payment/index.tsx +++ b/apps/dcellar-web-ui/src/utils/payment/index.tsx @@ -57,9 +57,7 @@ export const getQuotaNetflowRate = (size: number, storeFeeParams: StoreFeeParams }; export const getClientFrozen = (settleTime: number, bufferTime: number) => { - if (String(settleTime).length !== 13) { - return false; - } const curTime = getTimestampInSeconds(); + return curTime + bufferTime > settleTime; }; diff --git a/apps/dcellar-web-ui/src/utils/time.ts b/apps/dcellar-web-ui/src/utils/time.ts index ab2af74e..520b034f 100644 --- a/apps/dcellar-web-ui/src/utils/time.ts +++ b/apps/dcellar-web-ui/src/utils/time.ts @@ -40,9 +40,6 @@ export const convertTimeStampToDate = (utcTimestamp: number) => { }; export const formatTime = (utcZeroTimestamp = 0) => { - if (String(utcZeroTimestamp).length !== 13) { - return '--'; - } dayjs.extend(utc); dayjs.extend(timezone);