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`Session Cookies: These cookies exist only during your visit to the website and are automatically deleted when you close your browser. They are used to maintain session state, such as storing the contents of your shopping cart.`}
{t`Persistent Cookies: These cookies are stored on your device until they reach their expiration date or are manually deleted. They are used to remember your preferences and provide a personalized experience when you revisit the website.`}
{t`Third-Party Cookies: The Site may use cookies from third-party service providers for traffic analysis and user behavior tracking, as well as to display ads relevant to your interests.`}
-
+
{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`Connect Wallet`}
- )}
-
-
-
- {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`MAX`}
-
-
-
-
-
-
- {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 𝕏`}
+
+ {loading ? : '𝕏'}
+
+
+
+
+
+
+
+
+
+ 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`MAX`}
+
+
+
+
+
+
+ {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
-
-
-
- {t`Please connect first`}
-
-
-
+
+
+
+ The staking addresses need to pass Go+ security check. Note that staking is not available in some
+ restricted regions.
+ More
+
+
+
+ {t`Please connect first`}
+
+
)
}
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