diff --git a/electron/main.js b/electron/main.js index c820346d6..faf838883 100644 --- a/electron/main.js +++ b/electron/main.js @@ -236,7 +236,7 @@ const createMainWindow = () => { mainWindow.setSize(width, height); }); - ipcMain.on('show-notification', (title, description) => { + ipcMain.on('show-notification', (_event, title, description) => { showNotification(title, description || undefined); }); diff --git a/frontend/components/Main/KeepAgentRunning.tsx b/frontend/components/Main/KeepAgentRunning.tsx index e0ad0f5ee..c44b4bb44 100644 --- a/frontend/components/Main/KeepAgentRunning.tsx +++ b/frontend/components/Main/KeepAgentRunning.tsx @@ -9,6 +9,8 @@ import { CardSection } from '../styled/CardSection'; const { Text } = Typography; +const COVER_BLOCK_BORDERS_STYLE = { marginBottom: '-1px' }; + export const KeepAgentRunning = () => { const { storeState } = useStore(); const { serviceStatus } = useServices(); @@ -17,7 +19,7 @@ export const KeepAgentRunning = () => { if (serviceStatus !== DeploymentStatus.DEPLOYED) return null; return ( - + void; +}) => { + const { minimumStakedAmountRequired } = useReward(); + + if (!open) return null; + return ( + + Got it + , + ]} + > + + OLAS logo + + + {`Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`} + + Your agent is working towards earning rewards. + + Pearl is designed to make it easy for you to earn staking rewards every + day. Simply leave the app and agent running in the background for ~1hr a + day. + + + ); +}; + export const MainHeader = () => { const { storeState } = useStore(); const { services, serviceStatus, setServiceStatus } = useServices(); @@ -39,6 +88,11 @@ export const MainHeader = () => { setIsPaused: setIsBalancePollingPaused, } = useBalance(); + const [isModalOpen, setIsModalOpen] = useState(false); + const handleModalClose = useCallback(() => setIsModalOpen(false), []); + + const { minimumStakedAmountRequired } = useReward(); + const safeOlasBalanceWithStaked = useMemo(() => { if (safeBalance?.OLAS === undefined) return; if (totalOlasStakedBalance === undefined) return; @@ -115,6 +169,8 @@ export const MainHeader = () => { // }); // } + const serviceExists = !!services?.[0]; + // For now POST /api/services will take care of creating, starting and updating the service return ServicesService.createService({ serviceTemplate, @@ -122,7 +178,14 @@ export const MainHeader = () => { }) .then(() => { setServiceStatus(DeploymentStatus.DEPLOYED); - showNotification?.('Your agent is now running!'); + if (serviceExists) { + showNotification?.('Your agent is now running!'); + } else { + showNotification?.( + `Your agent is running and you've staked ${minimumStakedAmountRequired} OLAS!`, + ); + setIsModalOpen(true); + } }) .finally(() => { setIsBalancePollingPaused(false); @@ -134,11 +197,13 @@ export const MainHeader = () => { } }, [ masterSafeAddress, + minimumStakedAmountRequired, serviceTemplate, + services, setIsBalancePollingPaused, setServiceStatus, - wallets, showNotification, + wallets, ]); const handlePause = useCallback(() => { @@ -250,17 +315,19 @@ export const MainHeader = () => { ); })(); + const serviceExists = !!services?.[0]; + if (!isDeployable) { return ( ); } return ( ); }, [ @@ -279,6 +346,7 @@ export const MainHeader = () => { {agentHead} {serviceToggleButton} + ); }; diff --git a/frontend/components/Main/MainOlasBalance.tsx b/frontend/components/Main/MainOlasBalance.tsx index 39ac921e0..a3f356e71 100644 --- a/frontend/components/Main/MainOlasBalance.tsx +++ b/frontend/components/Main/MainOlasBalance.tsx @@ -1,16 +1,104 @@ -import { Skeleton } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Flex, Skeleton, Tooltip, Typography } from 'antd'; import { useMemo } from 'react'; import styled from 'styled-components'; import { balanceFormat } from '@/common-util/numberFormatters'; +import { COLOR } from '@/constants'; import { UNICODE_SYMBOLS } from '@/constants/unicode'; import { useBalance } from '@/hooks'; +import { useReward } from '@/hooks/useReward'; import { CardSection } from '../styled/CardSection'; +const { Text } = Typography; const Balance = styled.span` letter-spacing: -2px; + margin-right: 4px; `; +const BalanceBreakdown = styled.div` + padding: 4px; +`; +const BalanceBreakdownLine = styled.div` + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 14px; + color: ${COLOR.TEXT}; + + > span { + background: ${COLOR.WHITE}; + z-index: 1; + &:first-child { + padding-right: 6px; + } + &:last-child { + padding-left: 6px; + } + } + + &:before { + content: ''; + position: absolute; + bottom: 6px; + width: 100%; + border-bottom: 2px dotted ${COLOR.BORDER_GRAY}; + } + + &:not(:last-child) { + margin-bottom: 8px; + } +`; +const OVERLAY_STYLE = { maxWidth: '300px', width: '300px' }; + +const CurrentBalance = () => { + const { totalOlasBalance, totalOlasStakedBalance } = useBalance(); + const { accruedServiceStakingRewards } = useReward(); + + const balances = useMemo(() => { + return [ + { + title: 'Staked amount', + value: balanceFormat(totalOlasStakedBalance ?? 0, 2), + }, + { + title: 'Unclaimed rewards', + value: balanceFormat(accruedServiceStakingRewards ?? 0, 2), + }, + { + title: 'Unused funds', + value: balanceFormat( + (totalOlasBalance ?? 0) - (totalOlasStakedBalance ?? 0), + 2, + ), + }, + ]; + }, [accruedServiceStakingRewards, totalOlasBalance, totalOlasStakedBalance]); + + return ( + + Current balance  + + {balances.map((item, index) => ( + + {item.title} + {item.value} OLAS + + ))} + + } + > + + + + ); +}; export const MainOlasBalance = () => { const { isBalanceLoaded, totalOlasBalance } = useBalance(); @@ -21,12 +109,15 @@ export const MainOlasBalance = () => { }, [totalOlasBalance]); return ( - + {isBalanceLoaded ? ( <> - {UNICODE_SYMBOLS.OLAS} - {balance} - OLAS + + + {UNICODE_SYMBOLS.OLAS} + {balance} + OLAS + ) : ( diff --git a/frontend/components/Main/MainRewards.tsx b/frontend/components/Main/MainRewards.tsx index da0617a72..a28bb8d05 100644 --- a/frontend/components/Main/MainRewards.tsx +++ b/frontend/components/Main/MainRewards.tsx @@ -1,28 +1,18 @@ -import { Button, Col, Flex, Modal, Row, Skeleton, Tag, Typography } from 'antd'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Button, Flex, Modal, Skeleton, Tag, Tooltip, Typography } from 'antd'; import Image from 'next/image'; import { useCallback, useEffect, useState } from 'react'; -import styled from 'styled-components'; import { balanceFormat } from '@/common-util'; -import { COLOR } from '@/constants'; import { useBalance } from '@/hooks'; import { useElectronApi } from '@/hooks/useElectronApi'; import { useReward } from '@/hooks/useReward'; import { useStore } from '@/hooks/useStore'; import { ConfettiAnimation } from '../common/ConfettiAnimation'; +import { CardSection } from '../styled/CardSection'; -const { Text, Title } = Typography; - -const RewardsRow = styled(Row)` - margin: 0 -24px; - > .ant-col { - padding: 24px; - &:not(:last-child) { - border-right: 1px solid ${COLOR.BORDER_GRAY}; - } - } -`; +const { Text, Title, Paragraph } = Typography; const Loader = () => ( @@ -32,59 +22,40 @@ const Loader = () => ( ); const DisplayRewards = () => { - const { - availableRewardsForEpochEth, - isEligibleForRewards, - minimumStakedAmountRequired, - } = useReward(); - const { isBalanceLoaded, totalOlasStakedBalance } = useBalance(); - - // check if the staked amount is greater than the minimum required - const isStaked = - minimumStakedAmountRequired && - totalOlasStakedBalance && - totalOlasStakedBalance >= minimumStakedAmountRequired; + const { availableRewardsForEpochEth, isEligibleForRewards } = useReward(); + const { isBalanceLoaded } = useBalance(); return ( - - - - Staking rewards today - {isBalanceLoaded ? ( - <> - - {balanceFormat(availableRewardsForEpochEth, 2)} OLAS - - {isEligibleForRewards ? ( - Earned - ) : ( - Not yet earned - )} - - ) : ( - - )} - - - - - - Staked amount - {isBalanceLoaded ? ( - <> - - {balanceFormat(totalOlasStakedBalance, 2)} OLAS - - {minimumStakedAmountRequired && !isStaked ? ( - Not yet staked - ) : null} - + + + Staking rewards this work period  + + The agent's working period lasts at least 24 hours, but its + start and end point may not be at the same time every day. + + } + > + + + + {isBalanceLoaded ? ( + + + {balanceFormat(availableRewardsForEpochEth, 2)} OLAS  + + {isEligibleForRewards ? ( + Earned ) : ( - + Not yet earned )} - - + ) : ( + + )} + ); }; diff --git a/frontend/constants/color.ts b/frontend/constants/color.ts index 1c825de0f..7625d03ee 100644 --- a/frontend/constants/color.ts +++ b/frontend/constants/color.ts @@ -8,4 +8,5 @@ export const COLOR = { WHITE: '#ffffff', BORDER_GRAY: '#DFE5EE', BROWN: '#873800', + TEXT: '#1f2229', }; diff --git a/frontend/context/ElectronApiProvider.tsx b/frontend/context/ElectronApiProvider.tsx index 08327df01..032551c99 100644 --- a/frontend/context/ElectronApiProvider.tsx +++ b/frontend/context/ElectronApiProvider.tsx @@ -28,7 +28,7 @@ type ElectronApiContextProps = { saveLogs?: (data: { store?: ElectronStore; debugData?: Record; - }) => Promise<{ success: true; dirPath: string } | { success: false }>; + }) => Promise<{ success: true; dirPath: string } | { success?: false }>; openPath?: (filePath: string) => void; }; diff --git a/frontend/hooks/useReward.ts b/frontend/hooks/useReward.ts index d0222ba9b..f72b03edc 100644 --- a/frontend/hooks/useReward.ts +++ b/frontend/hooks/useReward.ts @@ -8,6 +8,7 @@ export const useReward = () => { availableRewardsForEpochEth, isEligibleForRewards, minimumStakedAmountRequired, + accruedServiceStakingRewards, } = useContext(RewardContext); return { @@ -15,5 +16,6 @@ export const useReward = () => { availableRewardsForEpochEth, isEligibleForRewards, minimumStakedAmountRequired, + accruedServiceStakingRewards, }; }; diff --git a/frontend/styles/globals.scss b/frontend/styles/globals.scss index a823f6f9d..13afcf16c 100644 --- a/frontend/styles/globals.scss +++ b/frontend/styles/globals.scss @@ -141,6 +141,10 @@ button, input, select, textarea, .ant-input-suffix { margin-right: auto !important; } +.text-xl { + font-size: 20px; +} + .text-base { font-size: 16px !important; } @@ -149,6 +153,10 @@ button, input, select, textarea, .ant-input-suffix { font-size: 14px !important; } +.text-center { + text-align: center !important; +} + .font-weight-600 { font-weight: 600 !important; }