Skip to content

Commit

Permalink
feat: initial implementation of wallet connect with ArConnect
Browse files Browse the repository at this point in the history
  • Loading branch information
kunstmusik committed Apr 26, 2024
1 parent efdccec commit 6f635a5
Show file tree
Hide file tree
Showing 18 changed files with 488 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"vis": "yarn vite-bundle-visualizer"
},
"dependencies": {
"@ar.io/sdk": "^1.0.2-alpha.1",
"@ar.io/sdk": "^1.0.2",
"@fontsource/rubik": "^5.0.19",
"@headlessui/react": "^1.7.19",
"@sentry/browser": "^7.101.1",
Expand Down
5 changes: 3 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import AppRouterLayout from './layout/AppRouterLayout';
import Loading from './pages/Loading';
import NotFound from './pages/NotFound';
import WalletProvider from './components/WalletProvider';

const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Gateways = React.lazy(() => import('./pages/Gateways'));
Expand Down Expand Up @@ -63,9 +64,9 @@ function App() {
);

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

Expand Down
6 changes: 5 additions & 1 deletion 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,6 +69,7 @@ export const Button = ({
].join(' ');
return (
<button
ref={forwardRef}
title={title}
className={buttonClassNames}
onClick={onClick}
Expand Down
80 changes: 77 additions & 3 deletions src/components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,84 @@
import { useState } from 'react';
import { Popover } from '@headlessui/react';
import { useGlobalState } from '@src/store';
import { formatWalletAddress } from '@src/utils';
import { forwardRef, useState } from 'react';
import Button, { ButtonType } from './Button';
import { ConnectIcon } from './icons';
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);

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

const wallet = useGlobalState((state) => state.wallet);
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)}
</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">
13.7k
</div>
<div className="px-[16px] pt-[12px] text-xs text-low">AR Balance</div>
<div className="px-[16px] pt-[4px] text-high">0.06</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}
Expand All @@ -17,6 +89,8 @@ const Profile = () => {
/>
<ConnectModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
</div>
) : (
<div></div>
);
};
export default Profile;
92 changes: 92 additions & 0 deletions src/components/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 { ReactElement, useEffect } from 'react';

const WalletProvider = ({ children }: { children: ReactElement }) => {
const { setWalletStateInitialized, updateWallet } = useGlobalState();

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

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

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

// useEffect(() => {
// if (walletAddress) {
// updateBalances(walletAddress);
// }
// }, [walletAddress, blockHeight]);

// const updateBalances = async (address: ArweaveTransactionID) => {
// try {
// const [ioBalance, arBalance] = await Promise.all([
// arweaveDataProvider.getTokenBalance(address, ARNS_REGISTRY_ADDRESS),
// arweaveDataProvider.getArBalance(address),
// ]);

// dispatchWalletState({
// type: 'setBalances',
// payload: {
// [ioTicker]: ioBalance,
// ar: arBalance,
// },
// });
// } catch (error) {
// eventEmitter.emit('error', error);
// }
// };

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

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

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

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

// const updateIfConnected = async () => {

// const walletType = window.localStorage.getItem('walletType');
// const globalState = useGlobalState();

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

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

// export const initializeArConnect = async () => {

// }

export default WalletProvider;
6 changes: 6 additions & 0 deletions src/components/icons/gear.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/components/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import ContractIcon from './contract.svg?react';
import DashboardIcon from './dashboard.svg?react';
import DocsIcon from './docs.svg?react';
import GatewaysIcon from './gateways.svg?react';
import GearIcon from './gear.svg?react';
import LinkArrowIcon from './link_arrow.svg?react';
import LogoutIcon from './logout.svg?react';
import OpenDrawerIcon from './open_drawer.svg?react';
import StakingIcon from './staking.svg?react';
import WalletIcon from './wallet.svg?react';

export {
ArConnectIcon,
Expand All @@ -24,7 +27,10 @@ export {
DashboardIcon,
DocsIcon,
GatewaysIcon,
GearIcon,
OpenDrawerIcon,
LinkArrowIcon,
LogoutIcon,
StakingIcon,
WalletIcon,
};
7 changes: 7 additions & 0 deletions src/components/icons/logout.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/components/icons/wallet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 41 additions & 1 deletion src/components/modals/ConnectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
/* eslint-disable tailwindcss/classnames-order */

import { Dialog } from '@headlessui/react';
import { useGlobalState } from '@src/store';
import { ArweaveWalletConnector } from '@src/types';
import { useState } from 'react';
import Button from '../Button';
import { ArConnectIcon, CloseIcon, ConnectIcon } from '../icons';
import { ArConnectWalletConnector } from '@src/services/wallets/ArConnectWalletConnector';

const ConnectModal = ({
open,
Expand All @@ -11,6 +15,38 @@ const ConnectModal = ({
open: boolean;
onClose: () => void;
}) => {
const [connecting, setConnecting] = useState<boolean>(false);

const updateWallet = useGlobalState((state) => state.updateWallet);

const connect = async (walletConnector: ArweaveWalletConnector) => {
try {
setConnecting(true);
await walletConnector.connect();

// const arweaveGate = await walletConnector.getGatewayConfig();
// if (arweaveGate?.host) {
// await dispatchNewGateway(
// arweaveGate.host,
// walletConnector,
// dispatchGlobalState,
// );
// }

const address = await walletConnector.getWalletAddress();

updateWallet(address.toString(), walletConnector);

onClose();
} catch (error: any) {
// if (walletConnector) {
// eventEmitter.emit('error', error);
// }
} finally {
setConnecting(false);
}
};

return (
<Dialog open={open} onClose={onClose} className="relative z-50">
<div
Expand All @@ -32,7 +68,11 @@ const ConnectModal = ({
</h2>
<div className="flex grow justify-center pb-[32px]">
<Button
onClick={() => {}}
onClick={() => {
if (!connecting) {
connect(new ArConnectWalletConnector());
}
}}
active={true}
icon={<ArConnectIcon className="size-[16px]" />}
title="Connect with ArConnect"
Expand Down
19 changes: 19 additions & 0 deletions src/hooks/useEffectOnce.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EffectCallback, useEffect, useRef } from 'react';

/** Hook for ensuring an effect is run only once for a component on first mount.
* Will not be run on subsequent unmount/remounts of the component. Should only be used
* for effects where cleanup code (i.e., the return Destructor value from the EffectCallback)
* is not needed.
*
* @param effect The effect to run once.
*/
export const useEffectOnce = (effect: EffectCallback) => {
const initializationOccuredRef = useRef(false);

useEffect(() => {
if (initializationOccuredRef.current === false) {
initializationOccuredRef.current = true;
effect();
}
});
};
Loading

0 comments on commit 6f635a5

Please sign in to comment.