diff --git a/electron/main.js b/electron/main.js
index 70a282a11..df500f4f9 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -60,6 +60,10 @@ let tray,
nextAppProcess,
nextAppProcessPid;
+function showNotification(title, body) {
+ new Notification({ title, body }).show();
+}
+
async function beforeQuit() {
if (operateDaemonPid) {
try {
@@ -220,12 +224,8 @@ const createMainWindow = () => {
mainWindow.setSize(width, height);
});
- ipcMain.on('notify-agent-running', () => {
- if (!mainWindow.isVisible()) {
- new Notification({
- title: 'Your agent is now running!',
- }).show();
- }
+ ipcMain.on('show-notification', (title, description) => {
+ showNotification(title, description || undefined);
});
mainWindow.webContents.on('did-fail-load', () => {
diff --git a/electron/preload.js b/electron/preload.js
index 3c30e0f16..47a516449 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -5,5 +5,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
closeApp: () => ipcRenderer.send('close-app'),
minimizeApp: () => ipcRenderer.send('minimize-app'),
setAppHeight: (height) => ipcRenderer.send('set-height', height),
- notifyAgentRunning: () => ipcRenderer.send('notify-agent-running'),
+ showNotification: (title, description) =>
+ ipcRenderer.send('show-notification', title, description),
});
diff --git a/frontend/components/Main/MainHeader.tsx b/frontend/components/Main/MainHeader.tsx
index 8992052d1..6a980a5e0 100644
--- a/frontend/components/Main/MainHeader.tsx
+++ b/frontend/components/Main/MainHeader.tsx
@@ -1,7 +1,6 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Badge, Button, Flex, Popover, Typography } from 'antd';
import { formatUnits } from 'ethers/lib/utils';
-import get from 'lodash/get';
import Image from 'next/image';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -9,6 +8,7 @@ import { Chain, DeploymentStatus } from '@/client';
import { setTrayIcon } from '@/common-util';
import { COLOR, LOW_BALANCE, SERVICE_TEMPLATES } from '@/constants';
import { useBalance, useServiceTemplates } from '@/hooks';
+import { useElectronApi } from '@/hooks/useElectronApi';
import { useServices } from '@/hooks/useServices';
import { useWallet } from '@/hooks/useWallet';
import { ServicesService } from '@/service';
@@ -25,13 +25,9 @@ enum ServiceButtonLoadingState {
NotLoading,
}
-const notifyAgentRunning = () => {
- const fn = get(window, 'electronAPI.notifyAgentRunning') ?? (() => null);
- return fn();
-};
-
export const MainHeader = () => {
const { services, serviceStatus, setServiceStatus } = useServices();
+ const { showNotification } = useElectronApi();
const { getServiceTemplates } = useServiceTemplates();
const { wallets, masterSafeAddress } = useWallet();
const {
@@ -118,7 +114,7 @@ export const MainHeader = () => {
setServiceStatus(DeploymentStatus.DEPLOYED);
setIsBalancePollingPaused(false);
setServiceButtonState(ServiceButtonLoadingState.NotLoading);
- notifyAgentRunning();
+ showNotification?.('Your agent is now running!');
});
} catch (error) {
setIsBalancePollingPaused(false);
@@ -130,6 +126,7 @@ export const MainHeader = () => {
setIsBalancePollingPaused,
setServiceStatus,
wallets,
+ showNotification,
]);
const handlePause = useCallback(() => {
diff --git a/frontend/components/Main/MainRewards.tsx b/frontend/components/Main/MainRewards.tsx
index 9e1b56f26..829464c2e 100644
--- a/frontend/components/Main/MainRewards.tsx
+++ b/frontend/components/Main/MainRewards.tsx
@@ -1,12 +1,17 @@
-import { Col, Flex, Row, Skeleton, Tag, Typography } from 'antd';
+import { Button, Col, Flex, Modal, Row, Skeleton, Tag, 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';
-const { Text } = Typography;
+import { ConfettiAnimation } from '../common/ConfettiAnimation';
+
+const { Text, Title } = Typography;
const RewardsRow = styled(Row)`
margin: 0 -24px;
@@ -70,8 +75,106 @@ const DisplayRewards = () => {
);
};
+const NotifyRewards = () => {
+ const { isEligibleForRewards, availableRewardsForEpochEth } = useReward();
+ const { totalOlasBalance } = useBalance();
+ const { showNotification } = useElectronApi();
+
+ const [canShowNotification, setCanShowNotification] = useState(false);
+
+ useEffect(() => {
+ // TODO: Implement this once state persistence is available
+ const hasAlreadyNotified = true;
+
+ if (!isEligibleForRewards) return;
+ if (hasAlreadyNotified) return;
+ if (!availableRewardsForEpochEth) return;
+
+ setCanShowNotification(true);
+ }, [isEligibleForRewards, availableRewardsForEpochEth, showNotification]);
+
+ // hook to show app notification
+ useEffect(() => {
+ if (!canShowNotification) return;
+
+ showNotification?.(
+ 'Your agent earned its first staking rewards!',
+ `Congratulations! Your agent just got the first reward for you! Your current balance: ${availableRewardsForEpochEth} OLAS`,
+ );
+ }, [canShowNotification, availableRewardsForEpochEth, showNotification]);
+
+ const closeNotificationModal = useCallback(() => {
+ setCanShowNotification(false);
+ // TODO: add setter for hasAlreadyNotified
+ }, []);
+
+ if (!canShowNotification) return null;
+
+ return (
+
+
+ Share on
+
+
+ ,
+ ]}
+ >
+
+
+
+
+
+
+
+ Your agent just earned the first reward!
+
+
+
+
+ Congratulations! Your agent just earned the first
+
+ {` ${balanceFormat(availableRewardsForEpochEth, 2)} OLAS `}
+
+ for you!
+
+
+
+ Your current balance:
+ {` ${balanceFormat(totalOlasBalance, 2)} OLAS `}
+
+
+ Keep it running to get even more!
+
+
+ );
+};
+
export const MainRewards = () => (
<>
+
>
);
diff --git a/frontend/components/common/ConfettiAnimation.jsx b/frontend/components/common/ConfettiAnimation.jsx
new file mode 100644
index 000000000..9e02463ff
--- /dev/null
+++ b/frontend/components/common/ConfettiAnimation.jsx
@@ -0,0 +1,43 @@
+import { useCallback, useRef } from 'react';
+import ReactCanvasConfetti from 'react-canvas-confetti';
+import { useInterval } from 'usehooks-ts';
+
+const canvasStyles = {
+ position: 'fixed',
+ pointerEvents: 'none',
+ width: '100%',
+ height: '100%',
+ top: 0,
+ left: 0,
+};
+
+export const ConfettiAnimation = () => {
+ const animationInstance = useRef(null);
+
+ const makeShot = useCallback((particleRatio, opts) => {
+ if (!animationInstance.current) return;
+
+ animationInstance.current({
+ ...opts,
+ origin: { y: 0.45 },
+ particleCount: Math.floor(200 * particleRatio),
+ });
+ }, []);
+
+ const fire = useCallback(() => {
+ makeShot(0.25, { spread: 26, startVelocity: 55 });
+ makeShot(0.2, { spread: 60 });
+ makeShot(0.35, { spread: 80, decay: 0.91, scalar: 0.8 });
+ makeShot(0.1, { spread: 100, startVelocity: 25, decay: 0.92, scalar: 1.2 });
+ makeShot(0.1, { spread: 100, startVelocity: 45 });
+ }, [makeShot]);
+
+ const getInstance = useCallback((instance) => {
+ animationInstance.current = instance;
+ }, []);
+
+ // Fire confetti every 2.5 seconds
+ useInterval(() => fire(), 2500);
+
+ return ;
+};
diff --git a/frontend/context/ElectronApiProvider.tsx b/frontend/context/ElectronApiProvider.tsx
index 7c88272f9..586c84a8b 100644
--- a/frontend/context/ElectronApiProvider.tsx
+++ b/frontend/context/ElectronApiProvider.tsx
@@ -5,12 +5,14 @@ type ElectronApiContextProps = {
setAppHeight?: (height: number) => void;
closeApp?: () => void;
minimizeApp?: () => void;
+ showNotification?: (title: string, body?: string) => void;
};
export const ElectronApiContext = createContext({
setAppHeight: undefined,
closeApp: undefined,
minimizeApp: undefined,
+ showNotification: undefined,
});
const getElectronApiFunction = (functionNameInWindow: string) => {
@@ -33,6 +35,7 @@ export const ElectronApiProvider = ({ children }: PropsWithChildren) => {
setAppHeight: getElectronApiFunction('setAppHeight'),
closeApp: getElectronApiFunction('closeApp'),
minimizeApp: getElectronApiFunction('minimizeApp'),
+ showNotification: getElectronApiFunction('showNotification'),
}}
>
{children}
diff --git a/frontend/hooks/useElectronApi.tsx b/frontend/hooks/useElectronApi.tsx
index 5dff88fc6..daec352bd 100644
--- a/frontend/hooks/useElectronApi.tsx
+++ b/frontend/hooks/useElectronApi.tsx
@@ -3,12 +3,13 @@ import { useContext } from 'react';
import { ElectronApiContext } from '@/context/ElectronApiProvider';
export const useElectronApi = () => {
- const { setAppHeight, closeApp, minimizeApp } =
+ const { setAppHeight, closeApp, minimizeApp, showNotification } =
useContext(ElectronApiContext);
return {
setAppHeight,
closeApp,
minimizeApp,
+ showNotification,
};
};
diff --git a/frontend/package.json b/frontend/package.json
index f97a7e7e6..d3770f4f1 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,7 @@
"next": "^14.2.3",
"react": "^18",
"react-dom": "^18",
+ "react-canvas-confetti": "1.2.1",
"sass": "^1.72.0",
"styled-components": "^6.1.8",
"usehooks-ts": "^2.14.0"
diff --git a/frontend/public/splash-robot-head.png b/frontend/public/splash-robot-head.png
new file mode 100644
index 000000000..d43fb2873
Binary files /dev/null and b/frontend/public/splash-robot-head.png differ
diff --git a/frontend/public/twitter.svg b/frontend/public/twitter.svg
new file mode 100644
index 000000000..f2ddba13f
--- /dev/null
+++ b/frontend/public/twitter.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/styles/globals.scss b/frontend/styles/globals.scss
index a2b493f58..f2a62c14f 100644
--- a/frontend/styles/globals.scss
+++ b/frontend/styles/globals.scss
@@ -85,6 +85,14 @@ button, input, select, textarea, .ant-input-suffix {
margin-bottom: auto !important;
}
+.mt-8 {
+ margin-top: 8px !important;
+}
+
+.mt-12 {
+ margin-top: 12px !important;
+}
+
.mx-auto {
margin-left: auto !important;
margin-right: auto !important;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index c79d3d245..c2f3c23ee 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -1349,6 +1349,11 @@
dependencies:
"@babel/types" "^7.20.7"
+"@types/canvas-confetti@1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.4.0.tgz#22127a1a9ed9d456e626d6e2b9a4d3b0a240e18b"
+ integrity sha512-Neq4mvVecrHmTdyo98EY5bnKCjkZGQ6Ma7VyOrxIcMHEZPmt4kfquccqfBMrpNrdryMHgk3oGQi7XtpZacltnw==
+
"@types/graceful-fs@^4.1.3":
version "4.1.9"
resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz"
@@ -2152,6 +2157,11 @@ caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz"
integrity sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==
+canvas-confetti@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.4.0.tgz#840f6db4a566f8f32abe28c00dcd82acf39c92bd"
+ integrity sha512-S18o4Y9PqI/uabdlT/jI3MY7XBJjNxnfapFIkjkMwpz6qNxLFZOm2b22OMf4ZYDL9lpNWI+Ih4fEMVPwO1KHFQ==
+
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
@@ -5209,6 +5219,14 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
rc-resize-observer "^1.0.0"
rc-util "^5.36.0"
+react-canvas-confetti@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/react-canvas-confetti/-/react-canvas-confetti-1.2.1.tgz#22ac64cbc478cf57cb5f61c130322e359b35389d"
+ integrity sha512-onNjQNkhQjrB2JVCeRpJW7o8VlBNJvo3Eht9zlQlofNLVvvOd1+PzBLU5Ev8n5sFBkTbKJmIUVG0eZnkAl5cpg==
+ dependencies:
+ "@types/canvas-confetti" "1.4.0"
+ canvas-confetti "1.4.0"
+
react-dom@^18:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"