Skip to content

Commit

Permalink
Merge pull request #5 from ar-io/PE-5967-wallet-connection-profile-me…
Browse files Browse the repository at this point in the history
…nu-ar-connect

PE-5967: wallet connection profile menu AR connect
  • Loading branch information
kunstmusik authored Apr 30, 2024
2 parents 5ab5e34 + 4b451d0 commit 0c7ada3
Show file tree
Hide file tree
Showing 26 changed files with 748 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.9.0
20.12.2
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
"vis": "yarn vite-bundle-visualizer"
},
"dependencies": {
"@ar.io/sdk": "^1.0.2-alpha.1",
"@ar.io/sdk": "^1.0.3",
"@fontsource/rubik": "^5.0.19",
"@headlessui/react": "^1.7.19",
"@sentry/browser": "^7.101.1",
"@sentry/react": "^7.101.1",
"arweave": "^1.15.0",
"axios": "^1.6.7",
"axios-retry": "^4.0.0",
"react": "^18.2.0",
Expand Down Expand Up @@ -80,7 +82,7 @@
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"prettier-plugin-tailwindcss": "^0.5.14",
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.1",
"ts-jest": "^29.1.2",
Expand Down
22 changes: 15 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
createRoutesFromElements,
} from 'react-router-dom';

import GlobalDataProvider from './components/GlobalDataProvider';
import WalletProvider from './components/WalletProvider';
import AppRouterLayout from './layout/AppRouterLayout';
import Loading from './pages/Loading';
import NotFound from './pages/NotFound';
Expand All @@ -32,40 +34,46 @@ function App() {
<Dashboard />
</Suspense>
}
/>,
/>
,
<Route
path="gateways"
element={
<Suspense fallback={<Loading />}>
<Gateways />
</Suspense>
}
/>,
/>
,
<Route
path="staking"
element={
<Suspense fallback={<Loading />}>
<Staking />
</Suspense>
}
/>,
/>
,
<Route
path="observers"
element={
<Suspense fallback={<Loading />}>
<Observers />
</Suspense>
}
/>,
/>
,
<Route path="*" element={<Navigate to="/" />} />
</Route>,
),
);

return (
<>
<RouterProvider router={router} />
</>
<GlobalDataProvider>
<WalletProvider>
<RouterProvider router={router} />
</WalletProvider>
</GlobalDataProvider>
);
}

Expand Down
8 changes: 6 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MouseEventHandler, ReactElement } from 'react';
import { LegacyRef, MouseEventHandler, ReactElement } from 'react';

export enum ButtonType {
PRIMARY = 'primary',
Expand All @@ -7,6 +7,7 @@ export enum ButtonType {
}

export const Button = ({
forwardRef,
className,
buttonType = ButtonType.SECONDARY,
icon,
Expand All @@ -16,6 +17,7 @@ export const Button = ({
active = false,
onClick,
}: {
forwardRef?: LegacyRef<HTMLButtonElement>;
className?: string;
buttonType?: ButtonType;
icon?: ReactElement;
Expand All @@ -33,6 +35,7 @@ export const Button = ({
>
<button
title={title}
ref={forwardRef}
className="inline-flex items-center justify-start
gap-[11px] rounded-md bg-btn-primary-base bg-gradient-to-b
from-btn-primary-gradient-start to-btn-primary-gradient-end
Expand Down Expand Up @@ -66,13 +69,14 @@ export const Button = ({
].join(' ');
return (
<button
ref={forwardRef}
title={title}
className={buttonClassNames}
onClick={onClick}
>
{icon}
{text && (
<div className="flex grow items-center space-x-[4px] text-left">
<div className="flex grow items-center space-x-[4px] text-left leading-none">
{text} {rightIcon}
</div>
)}
Expand Down
42 changes: 42 additions & 0 deletions src/components/GlobalDataProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffectOnce } from '@src/hooks/useEffectOnce';
import { useGlobalState } from '@src/store';
import { ReactElement, useEffect } from 'react';

const GlobalDataProvider = ({
children,
}: {
children: ReactElement;
}) => {
const twoMinutes = 120000;

const setBlockHeight = useGlobalState((state) => state.setBlockHeight);
const setCurrentEpoch = useGlobalState((state) => state.setCurrentEpoch);
const arweave = useGlobalState((state) => state.arweave);
const arioReadSDK = useGlobalState((state) => state.arIOReadSDK);

useEffectOnce(() => {
const update = async () => {
const currentEpoch = await arioReadSDK.getCurrentEpoch();
setCurrentEpoch(currentEpoch);
};

update();
});

useEffect(() => {
const updateBlockHeight = async () => {
const blockHeight = await (await arweave.blocks.getCurrent()).height;
setBlockHeight(blockHeight);
};
updateBlockHeight();
const interval = setInterval(updateBlockHeight, twoMinutes);

return () => {
clearInterval(interval);
};
});

return <>{children}</>;
};

export default GlobalDataProvider;
22 changes: 16 additions & 6 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,34 @@
import Button, { ButtonType } from './Button';
import { ConnectIcon } from './icons';
import { NBSP } from '@src/constants';
import { useGlobalState } from '@src/store';
import Profile from './Profile';

const Header = () => {
const blockHeight = useGlobalState((state) => state.blockHeight);
const currentEpoch = useGlobalState((state) => state.currentEpoch);

return (
<header className="mt-[24px] flex h-[72px] rounded-[12px] border-[1px] py-[16px] pl-[24px] pr-[16px] leading-[1.4] dark:border-transparent-100-8 dark:bg-grey-1000 dark:text-grey-300">
<header className="mt-[24px] flex h-[72px] rounded-[12px] border py-[16px] pl-[24px] pr-[16px] leading-[1.4] dark:border-transparent-100-8 dark:bg-grey-1000 dark:text-grey-300">
<div className="inline-flex h-[38px] flex-col items-start justify-start gap-1 border-r pr-6 dark:border-transparent-100-8">
<div className="text-[12px] text-high">15</div>
<div className="text-[12px] text-high">
{currentEpoch
? currentEpoch.epochPeriod.toLocaleString('en-US')
: NBSP}
</div>
<div className="pt-[4px] text-[12px] leading-none text-low">
AR.IO EPOCH
</div>
</div>
<div className="inline-flex h-[38px] flex-col items-start justify-start gap-1 border-r px-6 dark:border-transparent-100-8">
<div className="text-[12px] text-high">1,367,904</div>
<div className="text-[12px] text-high">
{blockHeight ? blockHeight.toLocaleString('en-US') : NBSP}
</div>
<div className="pt-[4px] text-[12px] leading-none text-low">
ARWEAVE BLOCK
</div>
</div>
<div className="grow" />
<div className="content-center">
<Button buttonType={ButtonType.PRIMARY} icon={<ConnectIcon />} title="Connect" text="Connect" onClick={() => {}} />
<Profile />
</div>
</header>
);
Expand Down
99 changes: 99 additions & 0 deletions src/components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Popover } from '@headlessui/react';
import { useGlobalState } from '@src/store';
import { formatBalance, formatWalletAddress } from '@src/utils';
import { forwardRef, useState } from 'react';
import Button, { ButtonType } from './Button';
import {
ConnectIcon,
GearIcon,
LogoutIcon,
StakingIcon,
WalletIcon,
} from './icons';
import ConnectModal from './modals/ConnectModal';

// eslint-disable-next-line react/display-name
const CustomPopoverButton = forwardRef<HTMLButtonElement>((props, ref) => {
return (
<Button
forwardRef={ref}
buttonType={ButtonType.PRIMARY}
icon={<ConnectIcon />}
title="Profile"
{...props}
/>
);
});

const Profile = () => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

const walletStateInitialized = useGlobalState(
(state) => state.walletStateInitialized,
);

const wallet = useGlobalState((state) => state.wallet);
const balances = useGlobalState((state) => state.balances);
const updateWallet = useGlobalState((state) => state.updateWallet);
const walletAddress = useGlobalState((state) => state.walletAddress);

return walletAddress ? (
<Popover className="relative">
<Popover.Button as={CustomPopoverButton} />

<Popover.Panel
className="absolute right-0 z-10 mt-[10px] w-[240px]
rounded-[12px] border border-grey-800 bg-grey-1000 px-[16px] text-sm shadow-md"
>
<div className="flex gap-[8px] py-[20px]">
<WalletIcon />
<div className="text-sm text-high">
{formatWalletAddress(walletAddress.toString())}
</div>
</div>
<div className="rounded-[6px] border border-grey-800 py-[12px]">
<div className="px-[16px] text-xs text-low">IO Balance</div>
<div className="border-b border-grey-800 px-[16px] pb-[12px] pt-[4px] text-high">
{formatBalance(balances.io)}
</div>
<div className="px-[16px] pt-[12px] text-xs text-low">AR Balance</div>
<div className="px-[16px] pt-[4px] text-high">
{formatBalance(balances.ar)}
</div>
</div>
<div className="flex flex-col gap-[12px] py-[12px] text-mid">
<button className="flex items-center gap-[8px]">
<GearIcon /> Gateway Management
</button>
<button className="flex items-center gap-[8px]">
<StakingIcon /> Delegated Staking
</button>
<button
className="flex items-center gap-[8px]"
title="Logout"
onClick={async () => {
await wallet?.disconnect();
updateWallet(undefined, undefined);
}}
>
<LogoutIcon /> Logout
</button>
</div>
</Popover.Panel>
</Popover>
) : walletStateInitialized ? (
<div>
<Button
buttonType={ButtonType.PRIMARY}
icon={<ConnectIcon />}
title="Connect"
text="Connect"
onClick={() => setIsModalOpen(true)}
/>
<ConnectModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
) : (
<div></div>
);
};
export default Profile;
79 changes: 79 additions & 0 deletions src/components/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { useEffectOnce } from '@src/hooks/useEffectOnce';
import { ArConnectWalletConnector } from '@src/services/wallets/ArConnectWalletConnector';
import { useGlobalState } from '@src/store';
import { WALLET_TYPES } from '@src/types';
import { mioToIo } from '@src/utils';
import { ArweaveTransactionID } from '@src/utils/ArweaveTransactionId';
import Ar from 'arweave/web/ar';
import { ReactElement, useEffect } from 'react';

const AR = new Ar();

const WalletProvider = ({ children }: { children: ReactElement }) => {
const blockHeight = useGlobalState((state) => state.blockHeight);
const walletAddress = useGlobalState((state) => state.walletAddress);
const setWalletStateInitialized = useGlobalState(
(state) => state.setWalletStateInitialized,
);
const updateWallet = useGlobalState((state) => state.updateWallet);
const setBalances = useGlobalState((state) => state.setBalances);
const arweave = useGlobalState((state) => state.arweave);
const arIOReadSDK = useGlobalState((state) => state.arIOReadSDK);

useEffect(() => {
window.addEventListener('arweaveWalletLoaded', updateIfConnected);

return () => {
window.removeEventListener('arweaveWalletLoaded', updateIfConnected);
};
});

useEffectOnce(() => {
setTimeout(() => {
setWalletStateInitialized(true);
}, 5000);
});

useEffect(() => {
if (walletAddress) {
const updateBalances = async (address: ArweaveTransactionID) => {
try {
const [mioBalance, winstonBalance] = await Promise.all([
arIOReadSDK.getBalance({ address: address.toString() }),
arweave.wallets.getBalance(address.toString()),
]);

const arBalance = +AR.winstonToAr(winstonBalance);
const ioBalance = mioToIo(mioBalance);

setBalances(arBalance, ioBalance);
} catch (error) {
// eventEmitter.emit('error', error);
}
};

updateBalances(walletAddress);
}
}, [walletAddress, blockHeight, arIOReadSDK, arweave, setBalances]);

const updateIfConnected = async () => {
const walletType = window.localStorage.getItem('walletType');

try {
if (walletType === WALLET_TYPES.ARCONNECT) {
const connector = new ArConnectWalletConnector();
const address = await connector?.getWalletAddress();

updateWallet(address, connector);
}
} catch (error) {
// eventEmitter.emit('error', error);
} finally {
setWalletStateInitialized(true);
}
};

return <>{children}</>;
};

export default WalletProvider;
Loading

0 comments on commit 0c7ada3

Please sign in to comment.