Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dcellar-web-ui): support reactive the frozen account #398

Merged
merged 3 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dcellar-web-ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<DCModal isOpen={isOpen} onClose={onClose} gaShowName="dc.renewal.modal.0.show">
<ModalCloseButton />
<ModalBody textAlign={'center'} mt={0}>
<Text fontSize={'24px'} fontWeight={600} lineHeight="150%" marginBottom={'8px'}>
DCellar Renewal Guide
</Text>
<Text color="#76808F" fontSize={'16px'} fontWeight="400">
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.
</Text>
<Flex flexDir={'column'} marginTop={32}>
{STEP_DATA.map((step, index) => {
return (
<>
<Step {...step} />
{index !== STEP_DATA.length - 1 && (
<Box
height={'32px'}
w={'1px'}
borderLeft={'1px dotted #AEB4BC'}
marginLeft={'9px'}
/>
)}
</>
);
})}
</Flex>
</ModalBody>
<ModalFooter flexDirection={'column'} mt={32} gap={16}>
<DCButton
size="md"
gaClickName="dc.wrongnet.gf_modal.swithtogf.click"
width={'100%'}
onClick={() => {
onNavigate(InternalRoutePaths.transfer_in);
}}
>
Transfer In
</DCButton>
</ModalFooter>
</DCModal>
);
};
27 changes: 27 additions & 0 deletions apps/dcellar-web-ui/src/components/RenewalNotification/Step.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Flex gap={12} position={'relative'} textAlign={'left'}>
<Flex
width={'18px'}
height={'18px'}
bg={'opacity1'}
borderRadius={'9px'}
justifyContent={'center'}
alignItems={'center'}
flexShrink={0}
>
<Box width={8} height={8} borderRadius={4} bg={'brand.brand6'} />
</Flex>
<Text fontWeight={500} flexShrink={0}>
Step {num}
</Text>
<Text>{description}</Text>
</Flex>
);
};
223 changes: 223 additions & 0 deletions apps/dcellar-web-ui/src/components/RenewalNotification/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
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 | string>(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];

if (!item || closeRenewalAddresses.includes(item.address)) {
continue;
}

if (item.status === EStreamRecordStatus.FROZEN) {
const renewalStoreFee = BigNumber(item.frozenNetflowRate).times(reserveTime).abs();
const node = (
<Flex justifyContent={'space-between'} w={'100%'} alignItems={'center'}>
<Flex gap={'4px'}>
<Flex alignItems={'center'} h={20}>
<IconFont type={'colored-error2'} w={'16'} />
</Flex>
<Box fontSize={'14px'} lineHeight={'20px'}>
Your{' '}
<Box as="span" fontWeight={600}>
{item.name}
</Box>{' '}
is frozen, associated storage services are currently limited. To avoid data loss,
please deposit at least{' '}
<Box fontWeight={600} as="span">
{renewalStoreFee.isLessThan(MIN_AMOUNT)
? MIN_AMOUNT
: renewalStoreFee.toFixed(8, 0)}{' '}
{displayTokenSymbol()}
</Box>{' '}
to reactive.{' '}
<Button
variant="link"
onClick={() => onNavDeposit(item.address)}
fontWeight={400}
fontSize={'14px'}
textDecoration={'underline'}
>
Deposit Now
</Button>
</Box>
</Flex>
<IconFont
cursor={'pointer'}
onClick={() => {
onCloseNotification(item.address);
}}
color="readable.secondary"
type="close"
w={'16'}
/>
</Flex>
);
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 = (
<Flex justifyContent={'space-between'} w={'100%'} alignItems={'center'}>
<Flex gap={'4px'}>
<Flex alignItems={'center'} h={20}>
<IconFont type={'warning'} w={'16'} />
</Flex>
<Box fontSize={'14px'}>
Your{' '}
<Box as="span" fontWeight={600}>
{item.name}
</Box>{' '}
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{' '}
<Box fontWeight={600} as="span">
{fee.isLessThan(MIN_AMOUNT) ? MIN_AMOUNT : fee.toFixed(8, 0)}{' '}
{displayTokenSymbol()}
</Box>{' '}
into your payment account or associated owner account.{' '}
<Button
variant="link"
onClick={() => onNavDeposit(item.address)}
fontWeight={400}
fontSize={'14px'}
textDecoration={'underline'}
>
Deposit Now
</Button>
</Box>
</Flex>
<IconFont
cursor={'pointer'}
onClick={() => {
onCloseNotification(item.address);
}}
color="readable.secondary"
type="close"
w={'16'}
/>
</Flex>
);
nodes.push({ type: 'warning', node });
}
}
return nodes;
}, [
loginAccount,
paymentAccountList,
address,
accountInfos,
bankBalance,
router,
onOpen,
dispatch,
closeRenewalAddresses,
reserveTime,
]);

return (
<>
<Flex flexDirection={'column'} gap={16} mb={16}>
{notifications.map((item, index) => (
<Flex
key={index}
color={item.type === 'danger' ? '#CA300E' : 'readable.label-normal'}
bgColor={item.type === 'danger' ? 'rgba(238, 57, 17, 0.1)' : 'opacity3'}
borderRadius={4}
padding={'8px 12px'}
gap={12}
>
{item.node}
</Flex>
))}
</Flex>
<RenewalGuideModal isOpen={isOpen} onClose={onClose} />
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ export const DiscontinueBanner = ({
borderRadius={'4px'}
background={bg}
color={color}
p={8}
p={'8px 12px'}
gap={'4px'}
>
{icon}
<Text
fontSize={'14px'}
marginLeft={'4px'}
overflow={'hidden'}
// textOverflow={'ellipsis'}
// whiteSpace={'nowrap'}
>
<Text fontSize={'14px'} overflow={'hidden'}>
{content}
</Text>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -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]);
Expand Down
Loading
Loading