diff --git a/bun.lockb b/bun.lockb index f831b79..4177c27 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 051334f..ea975a5 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "react-router-dom": "^6.15.0", "react-social-icons": "^6.17.0", "react-transition-group": "^4.4.5", - "siwe": "^2.3.2" + "siwe": "^2.3.2", + "tslog": "^4.9.3" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/src/App.scss b/src/App.scss index 1e2baa3..3a7c5ae 100644 --- a/src/App.scss +++ b/src/App.scss @@ -73,6 +73,8 @@ body { } .mp__btnConnect { + position: relative; + width: 100%; border-radius: 18px !important; @@ -106,6 +108,12 @@ body { .mp__iconGray { width: 12pt; } + + .icon-overlay { + position: absolute; + top: -6px; + right: -6px; + } } .MuiDrawer-paper { @@ -759,6 +767,7 @@ input[type=number] { .chipPreTge { background: linear-gradient(180deg, #EBB84F, #bc923b) !important; + p { color: black !important } @@ -917,6 +926,7 @@ input[type=number] { .iconRed { color: #da3a34 !important; + svg { color: #da3a34 !important; } diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx index 4d36698..6cbb1ea 100644 --- a/src/AuthContext.tsx +++ b/src/AuthContext.tsx @@ -20,17 +20,19 @@ * @copyright SKALE Labs 2024-Present */ -import React, { createContext, useState, useContext, useEffect, useCallback } from 'react' -import { useWagmiAccount } from '@skalenetwork/metaport' +import React, { createContext, useState, useContext, useEffect } from 'react' +import { Logger, type ILogObj } from 'tslog' import { SiweMessage } from 'siwe' +import { useWagmiAccount } from '@skalenetwork/metaport' +import { API_URL } from './core/constants' -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api' +const log = new Logger({ name: 'AuthContext' }) interface AuthContextType { isSignedIn: boolean handleSignIn: () => Promise handleSignOut: () => Promise - checkSignInStatus: () => Promise + handleAddressChange: () => Promise getSignInStatus: () => Promise } @@ -40,41 +42,56 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [isSignedIn, setIsSignedIn] = useState(false) const { address } = useWagmiAccount() - const checkSignInStatus = useCallback(async () => { + const handleAddressChange = async function () { + log.info(`Address changed: ${address}`) if (!address) { + log.warn('No address found, signing out') setIsSignedIn(false) return } try { const status = await getSignInStatus() if (status) { + log.info('User is already signed in') setIsSignedIn(true) } else { + log.info('User not signed in, initiating sign in process') await handleSignOut() + await handleSignIn() } } catch (error) { - console.error('Error checking sign-in status:', error) + log.error('Error checking sign-in status:', error) setIsSignedIn(false) } - }, [address]) + } const getSignInStatus = async (): Promise => { try { if (!address) return false + log.info(`Checking sign-in status for address: ${address}`) const response = await fetch(`${API_URL}/auth/status`, { credentials: 'include' }) const data = await response.json() - return data.isSignedIn && data.address && data.address.toLowerCase() === address.toLowerCase() + const isSignedIn = + data.isSignedIn && data.address && data.address.toLowerCase() === address.toLowerCase() + log.info(`Sign-in status: ${isSignedIn}`) + return isSignedIn } catch (error) { - console.error('Error checking sign-in status:', error) + log.error('Error checking sign-in status:', error) return false } } const handleSignIn = async () => { - if (!address) return + if (!address) { + log.warn('Cannot sign in: No address provided') + return + } + log.info(`Initiating sign in for address: ${address}`) try { + const nonce = await fetchNonce() + log.debug(`Fetched nonce: ${nonce}`) const message = new SiweMessage({ domain: window.location.host, address: address, @@ -82,12 +99,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children uri: window.location.origin, version: '1', chainId: 1, - nonce: await fetchNonce() + nonce: nonce }) + log.debug('SIWE message created') const signature = await window.ethereum.request({ method: 'personal_sign', params: [message.prepareMessage(), address] }) + log.debug('Message signed') const response = await fetch(`${API_URL}/auth/signin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -95,39 +114,51 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children credentials: 'include' }) if (response.ok) { + log.info('Sign in successful') setIsSignedIn(true) + } else { + log.error('Sign in failed', response.status, response.statusText) } } catch (error) { - console.error('Error signing in:', error) + log.error('Error signing in:', error) } } const handleSignOut = async () => { + log.info('Initiating sign out') try { - await fetch(`${API_URL}/auth/signout`, { + const response = await fetch(`${API_URL}/auth/signout`, { method: 'POST', credentials: 'include' }) + if (response.ok) { + log.info('Sign out successful') + } else { + log.error('Sign out failed', response.status, response.statusText) + } } catch (error) { - console.error('Error signing out:', error) + log.error('Error signing out:', error) } finally { setIsSignedIn(false) } } const fetchNonce = async () => { + log.debug('Fetching nonce') const response = await fetch(`${API_URL}/auth/nonce`) const data = await response.json() + log.debug(`Nonce received: ${data.nonce}`) return data.nonce } useEffect(() => { - checkSignInStatus() - }, [address, checkSignInStatus]) + log.info('Address changed, updating auth status') + handleAddressChange() + }, [address]) return ( {children} @@ -137,6 +168,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children export const useAuth = () => { const context = useContext(AuthContext) if (context === undefined) { + log.error('useAuth must be used within an AuthProvider') throw new Error('useAuth must be used within an AuthProvider') } return context diff --git a/src/components/AccountMenu.tsx b/src/components/AccountMenu.tsx index d37f4cc..21bb384 100644 --- a/src/components/AccountMenu.tsx +++ b/src/components/AccountMenu.tsx @@ -22,7 +22,6 @@ */ import { useState, type MouseEvent } from 'react' -import { Link } from 'react-router-dom' import Jazzicon, { jsNumberForAddress } from 'react-jazzicon' import Box from '@mui/material/Box' @@ -32,14 +31,19 @@ import Tooltip from '@mui/material/Tooltip' import Button from '@mui/material/Button' import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward' -import HistoryIcon from '@mui/icons-material/History' import SignalCellularAltOutlinedIcon from '@mui/icons-material/SignalCellularAltOutlined' import AccountCircleRoundedIcon from '@mui/icons-material/AccountCircleRounded' import LooksRoundedIcon from '@mui/icons-material/LooksRounded' +import LoginOutlinedIcon from '@mui/icons-material/LoginOutlined' +import LogoutOutlinedIcon from '@mui/icons-material/LogoutOutlined' +import FiberManualRecordRoundedIcon from '@mui/icons-material/FiberManualRecordRounded' import { cls, styles, cmn, RainbowConnectButton } from '@skalenetwork/metaport' +import { useAuth } from '../AuthContext' + export default function AccountMenu(props: any) { + const { isSignedIn, handleSignIn, handleSignOut } = useAuth() const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) const handleClick = (event: MouseEvent) => { @@ -76,13 +80,32 @@ export default function AccountMenu(props: any) { ) : ( - + ) }} diff --git a/src/components/ecosystem/FavoriteIconButton.tsx b/src/components/ecosystem/FavoriteIconButton.tsx index f98479d..df0a47c 100644 --- a/src/components/ecosystem/FavoriteIconButton.tsx +++ b/src/components/ecosystem/FavoriteIconButton.tsx @@ -21,7 +21,7 @@ * @copyright SKALE Labs 2024-Present */ -import React, { useEffect } from 'react' +import React from 'react' import { IconButton, Tooltip } from '@mui/material' import FavoriteIcon from '@mui/icons-material/Favorite' import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder' @@ -36,45 +36,32 @@ interface FavoriteIconButtonProps { const FavoriteIconButton: React.FC = ({ chainName, appName }) => { const { likedApps, toggleLikedApp, refreshLikedApps, getAppId } = useLikedApps() - const { isSignedIn, handleSignIn, getSignInStatus } = useAuth() + const { isSignedIn, handleSignIn } = useAuth() const { address } = useWagmiAccount() const { openConnectModal } = useConnectModal() const appId = getAppId(chainName, appName) const isLiked = likedApps.includes(appId) - const [asyncLike, setAsyncLike] = React.useState(false) - const handleToggleLike = async () => { - setAsyncLike(true) if (!address) { openConnectModal?.() return } - await toggleLikedApp(appId) - refreshLikedApps() - } - - const handleAsyncLike = async () => { if (!isSignedIn) { await handleSignIn() - const status = await getSignInStatus() - if (!status) { - console.log('Sign-in failed or was cancelled') - return - } + return } await toggleLikedApp(appId) + refreshLikedApps() } - useEffect(() => { - if (asyncLike) { - setAsyncLike(false) - handleAsyncLike() - } - }, [address, isSignedIn]) + const getTooltipTitle = () => { + if (!isSignedIn) return 'Sign in to add to favorites' + return isLiked ? 'Remove from favorites' : 'Add to favorites' + } return ( - + {isLiked ? ( diff --git a/src/components/ecosystem/Socials.tsx b/src/components/ecosystem/Socials.tsx index 87087e4..696df81 100644 --- a/src/components/ecosystem/Socials.tsx +++ b/src/components/ecosystem/Socials.tsx @@ -23,7 +23,7 @@ import React from 'react' import { IconButton, Tooltip } from '@mui/material' -import { LanguageRounded, WavesRounded, TrackChangesRounded } from '@mui/icons-material' +import { LanguageRounded, TrackChangesRounded } from '@mui/icons-material' import { SocialIcon } from 'react-social-icons/component' import 'react-social-icons/discord' import 'react-social-icons/github' @@ -32,6 +32,7 @@ import 'react-social-icons/x' import { cmn, cls } from '@skalenetwork/metaport' import { type types } from '@/core' import FavoriteIconButton from './FavoriteIconButton' +import SwellIcon from './SwellIcon' interface SocialButtonsProps { social?: types.AppSocials @@ -78,10 +79,7 @@ const SocialButtons: React.FC = ({ { key: 'swell', icon: ( - + ), title: 'Swell' } diff --git a/src/components/ecosystem/SwellIcon.tsx b/src/components/ecosystem/SwellIcon.tsx new file mode 100644 index 0000000..8ee3001 --- /dev/null +++ b/src/components/ecosystem/SwellIcon.tsx @@ -0,0 +1,79 @@ +/** + * @license + * SKALE portal + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * @file SwellIcon.tsx + * @copyright SKALE Labs 2024-Present + */ + +import React from 'react' +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon' + +interface CustomLogoIconProps extends SvgIconProps { + size?: 'small' | 'medium' | 'large' | number +} + +const CustomLogoIcon: React.FC = ({ size, ...props }) => { + let fontSize: string | number = 'medium' + + if (size === 'small') fontSize = '1.25rem' + else if (size === 'medium') fontSize = '1.5rem' + else if (size === 'large') fontSize = '2.1875rem' + else if (typeof size === 'number') fontSize = `${size}px` + + return ( + + + + + + + + ) +} + +export default CustomLogoIcon diff --git a/src/core/constants.ts b/src/core/constants.ts index 707df7b..c5ce2fe 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -101,3 +101,5 @@ export const SKALE_SOCIAL_LINKS = { swell: 'https://swell.skale.space/', website: 'https://skale.space/' } + +export const DEFAULT_SWELL_URL = 'https://swell.skale.space/' diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 92b2a29..af12684 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -25,7 +25,16 @@ import { useEffect, useMemo, useState } from 'react' import { Helmet } from 'react-helmet' import { useParams } from 'react-router-dom' -import { MetaportCore, fromWei, styles, cmn, cls, SkPaper } from '@skalenetwork/metaport' +import { + MetaportCore, + fromWei, + styles, + cmn, + cls, + SkPaper, + useWagmiAccount, + useConnectModal +} from '@skalenetwork/metaport' import { type types } from '@/core' import { Button, Grid } from '@mui/material' @@ -71,9 +80,13 @@ export default function App(props: { chainsMeta: types.ChainsMetadataMap }) { let { chain, app } = useParams() - const { likedApps, appLikes, toggleLikedApp, getAppId, getTrendingApps } = useLikedApps() + const { likedApps, appLikes, toggleLikedApp, getAppId, getTrendingApps, refreshLikedApps } = + useLikedApps() const { isSignedIn, handleSignIn } = useAuth() + const { address } = useWagmiAccount() + const { openConnectModal } = useConnectModal() + const newApps = useMemo( () => getRecentApps(props.chainsMeta, MAX_APPS_DEFAULT), [props.chainsMeta] @@ -113,11 +126,17 @@ export default function App(props: { const trendingAppIds = useMemo(() => getTrendingApps(), [getTrendingApps]) const isNew = isNewApp({ chain, app }, newApps) - const handleFavoriteClick = async () => { + const handleToggleLike = async () => { + if (!address) { + openConnectModal?.() + return + } if (!isSignedIn) { await handleSignIn() + return } await toggleLikedApp(appId) + refreshLikedApps() } const explorerUrl = getExplorerUrl(network, chain) @@ -206,7 +225,7 @@ export default function App(props: { className={cls(cmn.mbott10, 'btn btnSm')} variant="contained" startIcon={isLiked ? : } - onClick={handleFavoriteClick} + onClick={handleToggleLike} > {isLiked ? 'Favorite' : 'Add to favorites'} diff --git a/vercel.json b/vercel.json index 5d6eb3f..87f6bee 100644 --- a/vercel.json +++ b/vercel.json @@ -5,7 +5,7 @@ "headers": [ { "key": "Content-Security-Policy", - "value": "default-src 'none'; script-src 'self' 'sha256-9kFihJ0ZOM+GgbS9s6zRngAhk2HvawY3s9ThnvanBVU=' https://www.googletagmanager.com 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' www.googletagmanager.com * data:; connect-src 'self' https://portal-server-testnet.skalenodes.com https://portal-server.skalenodes.com https://portal-server.skaleserver.com https://chain-proxy.wallet.coinbase.com www.googletagmanager.com http://eth-node.skalenodes.com https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" + "value": "default-src 'none'; script-src 'self' 'sha256-9kFihJ0ZOM+GgbS9s6zRngAhk2HvawY3s9ThnvanBVU=' https://www.googletagmanager.com 'sha256-SNHZ9YXEiiZqb8C8s0qFvFzqzRfWcWHKYL4BGuapkm4=' 'sha256-B2Yvd5DSiyn3CHdK2XYukRft560++o3GfZ3FkbQ7cig=' https://app.geckoboard.com https://*.zendesk.com https://static.zdassets.com https://vercel.live; style-src 'self' 'unsafe-inline'; img-src 'self' www.googletagmanager.com * data:; connect-src 'self' https://portal-server-testnet.skalenodes.com https://portal-server.skalenodes.com https://portal-server.skaleserver.com https://chain-proxy.wallet.coinbase.com www.googletagmanager.com http://eth-node.skalenodes.com https://ethereum-rpc.publicnode.com wss://legacy-proxy.skaleserver.com wss://ethereum-holesky.publicnode.com https://legacy-proxy.skaleserver.com https://ethereum-holesky-rpc.publicnode.com https://raw.githubusercontent.com https://github.com https://skalenetwork.github.io wss://relay.walletconnect.com https://explorer-api.walletconnect.com https://cloudflare-eth.com https://ethereum.publicnode.com wss://ethereum.publicnode.com wss://mainnet.skalenodes.com https://mainnet.skalenodes.com https://vercel.live wss://www.walletlink.org https://app.geckoboard.com https://*.zendesk.com https://ekr.zdassets.com https://ekr.zendesk.com https://*.zopim.com https://zendesk-eu.my.sentry.io wss://*.zendesk.com wss://*.zopim.com https://api.coingecko.com https://ethgasstation.info https://*.infura.io https://*.skalenodes.com; font-src 'self'; object-src 'none'; frame-src https://verify.walletconnect.org https://global.transak.com https://global-stg.transak.com https://verify.walletconnect.com https://app.geckoboard.com; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; manifest-src 'self';" }, { "key": "X-Content-Type-Options",