diff --git a/package.json b/package.json index 296c338..5d53f4f 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,8 @@ }, "pnpm": { "patchedDependencies": { - "urlcat@3.1.0": "patches/urlcat@3.1.0.patch" + "urlcat@3.1.0": "patches/urlcat@3.1.0.patch", + "react-use@17.5.0": "patches/react-use@17.5.0.patch" } } } diff --git a/patches/react-use@17.5.0.patch b/patches/react-use@17.5.0.patch new file mode 100644 index 0000000..328a78d --- /dev/null +++ b/patches/react-use@17.5.0.patch @@ -0,0 +1,87 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +deleted file mode 100644 +index 9b28df0e8aa4734af2c7006fd077ff17e2281db5..0000000000000000000000000000000000000000 +diff --git a/esm/useAsync.js b/esm/useAsync.js +index ddb0bb0370fc30090cf9a0ecc74bc039800a89ed..95a7cb8cc6833790a5aef21a64ee0c5d299723cf 100644 +--- a/esm/useAsync.js ++++ b/esm/useAsync.js +@@ -6,7 +6,11 @@ export default function useAsync(fn, deps) { + loading: true, + }), state = _a[0], callback = _a[1]; + useEffect(function () { +- callback(); ++ try { ++ callback(); ++ } catch (e) { ++ // Do nothing ++ } + }, [callback]); + return state; + } +diff --git a/esm/useAsyncFn.js b/esm/useAsyncFn.js +index 01d7307bd106229f6d791d0c588589235f404d97..89fce78d49b6332a1aa5dcd89e95ccbc0d02bca4 100644 +--- a/esm/useAsyncFn.js ++++ b/esm/useAsyncFn.js +@@ -13,15 +13,16 @@ export default function useAsyncFn(fn, deps, initialState) { + args[_i] = arguments[_i]; + } + var callId = ++lastCallId.current; +- if (!state.loading) { +- set(function (prevState) { return (__assign(__assign({}, prevState), { loading: true })); }); +- } ++ set(function (prevState) { ++ if (prevState.loading) return prevState ++ return (__assign(__assign({}, prevState), { loading: true })); ++ }); + return fn.apply(void 0, args).then(function (value) { + isMounted() && callId === lastCallId.current && set({ value: value, loading: false }); + return value; + }, function (error) { + isMounted() && callId === lastCallId.current && set({ error: error, loading: false }); +- return error; ++ throw error; + }); + }, deps); + return [state, callback]; +diff --git a/lib/useAsync.js b/lib/useAsync.js +index 7f189a49dea552b5b10d7380b982bfe84299a7a2..4d9d33acaad290b54a9ef6e7df0afdba56484972 100644 +--- a/lib/useAsync.js ++++ b/lib/useAsync.js +@@ -9,7 +9,11 @@ function useAsync(fn, deps) { + loading: true, + }), state = _a[0], callback = _a[1]; + react_1.useEffect(function () { +- callback(); ++ try { ++ callback(); ++ } catch (e) { ++ // Do nothing ++ } + }, [callback]); + return state; + } +diff --git a/lib/useAsyncFn.js b/lib/useAsyncFn.js +index e06fd819ccad625d709fa9907e946a9b8bc58543..6950e84a32ca630ec159834a87b4e21a36f4ef97 100644 +--- a/lib/useAsyncFn.js ++++ b/lib/useAsyncFn.js +@@ -15,15 +15,16 @@ function useAsyncFn(fn, deps, initialState) { + args[_i] = arguments[_i]; + } + var callId = ++lastCallId.current; +- if (!state.loading) { +- set(function (prevState) { return (tslib_1.__assign(tslib_1.__assign({}, prevState), { loading: true })); }); +- } ++ set(function (prevState) { ++ if (prevState.loading) return prevState ++ return (tslib_1.__assign(tslib_1.__assign({}, prevState), { loading: true })); ++ }); + return fn.apply(void 0, args).then(function (value) { + isMounted() && callId === lastCallId.current && set({ value: value, loading: false }); + return value; + }, function (error) { + isMounted() && callId === lastCallId.current && set({ error: error, loading: false }); +- return error; ++ throw error; + }); + }, deps); + return [state, callback]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2c3c32..4f6febd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false patchedDependencies: + react-use@17.5.0: + hash: hl7dr6rjk7dvc67mgwhhnozk2u + path: patches/react-use@17.5.0.patch urlcat@3.1.0: hash: iexdafwbwqknhkxkkvrkaj5nda path: patches/urlcat@3.1.0.patch @@ -57,7 +60,7 @@ dependencies: version: 18.3.1(react@18.3.1) react-use: specifier: ^17.5.0 - version: 17.5.0(react-dom@18.3.1)(react@18.3.1) + version: 17.5.0(patch_hash=hl7dr6rjk7dvc67mgwhhnozk2u)(react-dom@18.3.1)(react@18.3.1) urlcat: specifier: ^3.1.0 version: 3.1.0(patch_hash=iexdafwbwqknhkxkkvrkaj5nda) @@ -10365,7 +10368,7 @@ packages: tslib: 2.6.2 dev: false - /react-use@17.5.0(react-dom@18.3.1)(react@18.3.1): + /react-use@17.5.0(patch_hash=hl7dr6rjk7dvc67mgwhhnozk2u)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==} peerDependencies: react: '*' @@ -10388,6 +10391,7 @@ packages: ts-easing: 0.2.0 tslib: 2.6.2 dev: false + patched: true /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} diff --git a/src/components/Footer/Terms/index.tsx b/src/components/Footer/Terms/index.tsx index d5823a1..112c7c3 100644 --- a/src/components/Footer/Terms/index.tsx +++ b/src/components/Footer/Terms/index.tsx @@ -12,14 +12,14 @@ const CookiePolicyContent = () => (
{t`Cookies are small text files that are stored on your computer or mobile device when you visit a website. They are used to store information about you and your internet usage habits to improve your online experience. Cookies enable websites to recognize your device and remember some information about your preferences.`}

-

+

{t`Types of Cookies We Use`} -

+

{t`How to Control Cookies`}
diff --git a/src/components/StakeMaskStatusCard/ActivityStatusTag.tsx b/src/components/StakeMaskStatusCard/ActivityStatusTag.tsx index f53af34..36a8cf0 100644 --- a/src/components/StakeMaskStatusCard/ActivityStatusTag.tsx +++ b/src/components/StakeMaskStatusCard/ActivityStatusTag.tsx @@ -1,7 +1,12 @@ -import type { FC } from 'react' -import { Box, type BoxProps } from '@chakra-ui/react' +import { useMemo, type FC } from 'react' +import { Box, Skeleton, type BoxProps } from '@chakra-ui/react' +import { usePoolInfo } from '../../hooks/usePoolInfo' +import dayjs from 'dayjs' +import { t } from '@lingui/macro' export const ActivityStatusTag: FC = ({ ...props }) => { + const { data: pool, isLoading } = usePoolInfo() + const isStarted = useMemo(() => (pool ? dayjs(pool.start_time * 1000).isBefore(Date.now()) : false), [pool]) return ( = ({ ...props }) => { px="6px" {...props} > - Not started + {isLoading || !pool ? : isStarted ? t`On going` : t`Not started`} ) } diff --git a/src/components/StakeMaskStatusCard/index.tsx b/src/components/StakeMaskStatusCard/index.tsx index b4fb82e..4817c33 100644 --- a/src/components/StakeMaskStatusCard/index.tsx +++ b/src/components/StakeMaskStatusCard/index.tsx @@ -1,5 +1,19 @@ -import { Box, BoxProps, Button, Flex, Grid, HStack, Heading, Icon, Stack, Text, VStack } from '@chakra-ui/react' -import { t } from '@lingui/macro' +import { + Box, + BoxProps, + Button, + Flex, + Grid, + HStack, + Heading, + Icon, + Skeleton, + Stack, + Text, + Tooltip, + VStack, +} from '@chakra-ui/react' +import { Trans, t } from '@lingui/macro' import { FC } from 'react' import MaskLogoSVG from '../../assets/mask-logo.svg?react' import No1SVG from '../../assets/no-1.svg?react' @@ -8,13 +22,19 @@ import QuestionSVG from '../../assets/question.svg?react' import RightArrow from '../../assets/right-arrow.svg?react' import Rss3EthSVG from '../../assets/rss3-eth.svg?react' import TonEthSVG from '../../assets/ton-eth.svg?react' +import { formatNumber } from '../../helpers/formatNumber.ts' +import { formatSeconds } from '../../helpers/formatSeconds.ts' +import { usePoolInfo } from '../../hooks/usePoolInfo.ts' import { stakeModal } from '../../modals/index.tsx' import { ActivityStatusTag } from './ActivityStatusTag.tsx' -import { Tooltip } from '../Tooltip.tsx' export interface StakeMaskStatusCardProps extends BoxProps {} export const StakeMaskStatusCard: FC = ({ ...props }) => { + const { data: pool, isLoading } = usePoolInfo() + const rewardTokens = pool ? Object.values(pool.reward_pool) : [] + const rss3 = rewardTokens.find((x) => x.name === 'rss3') + const ton = rewardTokens.find((x) => x.name === 'ton') return ( = ({ ...props }) lineHeight={{ base: 6, lg: '140%' }} align="center" > - Time 3.20 2024~8.20 2024 + + Time + {isLoading || !pool ? ( + + ) : ( + `${formatSeconds(pool?.start_time, 'M.DD YYYY')}~${formatSeconds(pool.end_time, 'M.DD YYYY')}` + )} + @@ -99,9 +126,13 @@ export const StakeMaskStatusCard: FC = ({ ...props }) - - 700,000 - + {rss3 ? ( + + {formatNumber(+rss3.amount)} + + ) : ( + + )} RSS3 @@ -111,9 +142,14 @@ export const StakeMaskStatusCard: FC = ({ ...props }) - - 40,000 - + {ton ? ( + + {formatNumber(+ton.amount)} + + ) : ( + + )} + TON @@ -133,9 +169,22 @@ export const StakeMaskStatusCard: FC = ({ ...props }) p={6} spacing={6} > - - 12.2% - + {pool?.apr ? ( + + + {formatNumber(+pool.apr * 110, 2)}% + + + ) : ( + + )} {t`APR`} @@ -158,7 +207,13 @@ export const StakeMaskStatusCard: FC = ({ ...props }) color="neutrals.8" letterSpacing="-0.32px" > - 1,234,342 + {pool?.amount ? ( + + {formatNumber(+pool.amount)} + + ) : ( + + )} diff --git a/src/helpers/formatSeconds.ts b/src/helpers/formatSeconds.ts new file mode 100644 index 0000000..4902883 --- /dev/null +++ b/src/helpers/formatSeconds.ts @@ -0,0 +1,5 @@ +import dayjs from 'dayjs' + +export function formatSeconds(seconds: number, pattern: string) { + return dayjs(seconds * 1000).format(pattern) +} diff --git a/src/hooks/useLinkTwitter.ts b/src/hooks/useLinkTwitter.ts new file mode 100644 index 0000000..fe35b0e --- /dev/null +++ b/src/hooks/useLinkTwitter.ts @@ -0,0 +1,49 @@ +import { useAsyncFn } from 'react-use' +import urlcat from 'urlcat' +import { useAccount, useClient } from 'wagmi' +import { signMessage } from 'wagmi/actions' +import { config } from '../configs/wagmiClient' +import { FIREFLY_API_ROOT } from '../constants/api' +import { fetchJSON } from '../helpers/fetchJSON' +import { TwitterAuthorizeResponse } from '../types/api' +import { useToast } from '@chakra-ui/react' +import { UserRejectedRequestError } from 'viem' + +// Any message is ok. +const message = 'Hello, world!' +export function useLinkTwitter() { + const account = useAccount() + const client = useClient() + const toast = useToast() + + return useAsyncFn(async () => { + if (!account.address || !client) return + try { + const signed = await signMessage(config, { + account: account.address, + message: message, + }) + const url = urlcat(FIREFLY_API_ROOT, '/v1/mask_stake/twitter/authorize', { + original_message: message, + signature_message: signed.slice(2), // omit 0x + wallet_address: account.address, + }) + const res = await fetchJSON(url) + if (res.code !== 200) { + console.error('Failed to get twitter authorize', res.message, res.reason) + return + } + location.href = res.data.url + } catch (err) { + if (err instanceof UserRejectedRequestError) { + toast({ + status: 'error', + position: 'top-right', + title: err.details, + }) + return + } + throw err + } + }, [account.address, client]) +} diff --git a/src/modals/BaseModal.tsx b/src/modals/BaseModal.tsx index 72f7bf9..7085c74 100644 --- a/src/modals/BaseModal.tsx +++ b/src/modals/BaseModal.tsx @@ -1,4 +1,10 @@ import { + Box, + Drawer, + DrawerBody, + DrawerContent, + DrawerHeader, + DrawerOverlay, IconButton, Modal, ModalBody, @@ -8,6 +14,7 @@ import { ModalOverlay, ModalProps, Text, + useBreakpointValue, } from '@chakra-ui/react' import { t } from '@lingui/macro' @@ -20,22 +27,46 @@ interface Props extends ModalProps { height: ModalContentProps['height'] } export function BaseModal({ title, width, height, ...rest }: Props) { + const isMobile = useBreakpointValue({ base: true, md: false }) + console.log({ isMobile }) + const header = ( + <> + {title} + } + onClick={rest.onClose} + /> + + ) + if (isMobile) { + return ( + + + + + + {header} + + + {rest.children} + + + + + ) + } return ( - - {title} - } - onClick={rest.onClose} - /> + + {header} {rest.children} diff --git a/src/modals/StakeModal.tsx b/src/modals/StakeModal.tsx index 8f7a451..4db9b4b 100644 --- a/src/modals/StakeModal.tsx +++ b/src/modals/StakeModal.tsx @@ -9,26 +9,25 @@ import { Link, List, ListItem, - ModalBody, - ModalCloseButton, - ModalHeader, ModalProps, Skeleton, + Spinner, Stack, Text, VStack, } from '@chakra-ui/react' import { Trans, t } from '@lingui/macro' +import dayjs from 'dayjs' +import { useState } from 'react' import { useAccount, useBalance } from 'wagmi' import { StepIcon } from '../components/StepIcon' import { TokenIcon } from '../components/TokenIcon' -import { usePoolInfo } from '../hooks/usePoolInfo' -import dayjs from 'dayjs' import { formatNumber } from '../helpers/formatNumber' -import { useState } from 'react' +import { useLinkTwitter } from '../hooks/useLinkTwitter' +import { usePoolInfo } from '../hooks/usePoolInfo' import { usePoolStore } from '../store/poolStore' import { Tooltip } from '../components/Tooltip.tsx' -import { ModalWithDrawer } from '../components/ModalWithDrawer' +import { BaseModal } from './BaseModal' interface Props extends ModalProps {} @@ -41,157 +40,160 @@ export function StakeModal(props: Props) { address: account.address, token: maskTokenAddress, }) + const [{ loading }, linkTwitter] = useLinkTwitter() return ( - - - {t`Stake`} - - - - - - {t`Connect Wallet`} - {account.isConnected ? null : ( - - )} - - - - {t`Link 𝕏`} + + + + + + {t`Connect Wallet`} + {account.isConnected ? null : ( - - - - - - - - - Mask - - - Ethereum - - - - { - setAmount(e.currentTarget.value) - }} - _focus={{ outline: 'none', border: 'none' }} - _focusVisible={{ border: 'none', boxShadow: 'none' }} - /> - - - - - Balance:{' '} - {balance.isPending ? ( - - ) : ( - balance.data?.value.toLocaleString() - )} - - - - - - - - - - {t`Unlock MASK Time`} - {pool?.end_time ? ( - - {dayjs(pool.end_time * 1000).format('hh:mm d/MM/YYYY')} - - ) : ( - - )} - - - {t`APR`} - {pool?.apr ? ( - - {formatNumber(+pool.apr * 110, 2)}% + >{t`Connect Wallet`} + )} + + + + {t`Link 𝕏`} + + + + + + + + + + Mask + + + Ethereum + + + + { + setAmount(e.currentTarget.value) + }} + _focus={{ outline: 'none', border: 'none' }} + _focusVisible={{ border: 'none', boxShadow: 'none' }} + /> + + + + + Balance:{' '} + + {balance.isPending ? null : balance.data?.value.toLocaleString()} + + + + + + + + + + + {t`Unlock MASK Time`} + {pool?.end_time ? ( + + {dayjs(pool.end_time * 1000).format('hh:mm d/MM/YYYY')} + + ) : ( + + )} + + + {t`APR`} + {pool?.apr ? ( + + {formatNumber(+pool.apr * 110, 2)}% + + ) : ( + + )} + + + {t`Share of Pool`} + {amount && pool?.amount !== undefined ? ( + {formatNumber((+amount / +pool?.amount) * 100, 2)}% + ) : ( + + )} + + + {t`Pool Liquidity`} + + + {pool?.amount ? ( + + {formatNumber(+pool.amount)} ) : ( )} - - {t`Share of Pool`} - {amount && pool?.amount !== undefined ? ( - {formatNumber((+amount / +pool?.amount) * 100, 2)}% - ) : ( - - )} - - - {t`Pool Liquidity`} - - - {pool?.amount ? ( - - {formatNumber(+pool.amount)} - - ) : ( - - )} - - - - - The staking addresses need to pass Go+ security check. Note that staking is not available in some - restricted regions. - More - - - - - - - + + + + The staking addresses need to pass Go+ security check. Note that staking is not available in some + restricted regions. + More + + + + + + ) } diff --git a/src/styles/index.css b/src/styles/index.css index e539802..0134147 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -19,6 +19,7 @@ body { } .gradient-border::after { + pointer-events: none; content: ''; position: absolute; left: 0; diff --git a/src/types/api.ts b/src/types/api.ts index 908276a..b20ce15 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -60,3 +60,9 @@ export interface PoolInfo { } export type PoolInfoResponse = Response + +export interface TwitterAuthorizeResult { + url: string +} + +export type TwitterAuthorizeResponse = Response