Skip to content

Commit

Permalink
Feat: DAO Wallet Connect Main Flow (#908)
Browse files Browse the repository at this point in the history
* wallet connect nearly done

* fixes wip

* fixes wip

* review fixes

* review changes
  • Loading branch information
RakeshUP authored Jul 10, 2023
1 parent 707eb49 commit 0ba64fa
Show file tree
Hide file tree
Showing 8 changed files with 538 additions and 56 deletions.
3 changes: 3 additions & 0 deletions packages/web-app/src/containers/actionBuilder/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ const Action: React.FC<ActionsComponentProps> = ({
);
case 'wallet_connect_modal':
return <WalletConnect actionIndex={actionIndex} />;
case 'wallet_connect_action':
//TODO: Create a separate action-builder accordion for Wallet Connect Actions to handle the non-decodable flow and AlertCards
return <SCCAction actionIndex={actionIndex} allowRemove={allowRemove} />;
default:
throw Error('Action not found');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
IconChevronRight,
IconClose,
IconHome,
IconType,
} from '@aragon/ui-components';
import React from 'react';
import {useFormContext} from 'react-hook-form';
Expand All @@ -13,6 +14,7 @@ type DesktopModalHeaderProps = {
onClose?: () => void;
selectedContract?: string; // Note: may come from form, not set in stone
onSearch?: (search: string) => void;
buttonIcon?: React.FunctionComponentElement<IconType>;
};

const DesktopModalHeader: React.FC<DesktopModalHeaderProps> = props => {
Expand All @@ -23,7 +25,7 @@ const DesktopModalHeader: React.FC<DesktopModalHeaderProps> = props => {
<Container>
<LeftContent>
<ButtonIcon
icon={<IconHome />}
icon={props.buttonIcon || <IconHome />}
mode="secondary"
bgWhite
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import React, {useState} from 'react';
import styled from 'styled-components';
import {SessionTypes} from '@walletconnect/types';
import {useTranslation} from 'react-i18next';
import {AvatarDao, ButtonText, Spinner, Tag} from '@aragon/ui-components';
import {useFormContext} from 'react-hook-form';

import ModalBottomSheetSwitcher from 'components/modalBottomSheetSwitcher';
import ModalHeader from 'components/modalHeader';
import useScreen from 'hooks/useScreen';
import {useWalletConnectInterceptor} from 'hooks/useWalletConnectInterceptor';
import {WcRequest} from 'services/walletConnectInterceptor';
import {useActionsContext} from 'context/actions';
import {getEtherscanVerifiedContract} from 'services/etherscanAPI';
import {useNetwork} from 'context/network';
import {addABI, decodeMethod} from 'utils/abiDecoder';

type Props = {
onBackButtonClicked: () => void;
onClose: () => void;
isOpen: boolean;
selectedSession: SessionTypes.Struct;
actionIndex: number;
};

const ActionListenerModal: React.FC<Props> = ({
onBackButtonClicked,
onClose,
actionIndex,
selectedSession,
isOpen,
}) => {
const {isDesktop} = useScreen();
const {network} = useNetwork();
const {t} = useTranslation();
const [actionsReceived, setActionsReceived] = useState<Array<WcRequest>>([]);
const {addAction, removeAction} = useActionsContext();
const {setValue, resetField} = useFormContext();

function onActionRequest(request: WcRequest) {
setActionsReceived([...actionsReceived, request]);
}
const {wcDisconnect} = useWalletConnectInterceptor({
onActionRequest: onActionRequest,
});

/*************************************************
* Callbacks and Handlers *
*************************************************/
const handleAddActions = () => {
resetField(`actions.${actionIndex}`);

actionsReceived.map(async action => {
const etherscanData = await getEtherscanVerifiedContract(
action.params[0].to,
network
);

if (
etherscanData.status === '1' &&
etherscanData.result[0].ABI !== 'Contract source code not verified'
) {
addABI(JSON.parse(etherscanData.result[0].ABI));
const decodedData = decodeMethod(action.params[0].data);

if (decodedData) {
addAction({
name: 'external_contract_action',
});
setValue(`actions.${actionIndex}.name`, 'wallet_connect_action');
setValue(
`actions.${actionIndex}.contractAddress`,
action.params[0].to
);
setValue(
`actions.${actionIndex}.contractName`,
etherscanData.result[0].ContractName
);
setValue(`actions.${actionIndex}.functionName`, decodedData.name);
setValue(`actions.${actionIndex}.inputs`, decodedData.params);
// TODO: Add NatSpec
// setValue(`actions.${actionIndex}.notice`, );
} else {
//TODO: Failed to decode flow
}
} else {
//TODO: Failed to fetch ABI - failed to decode flow
}
});

removeAction(actionIndex);
};

/*************************************************
* Render *
*************************************************/
if (!isOpen) {
return null;
}

const metadataName = selectedSession.peer.metadata.name;
const metadataIcon = selectedSession.peer.metadata.icons[0];
const metadataURL = selectedSession.peer.metadata.url;

return (
<ModalBottomSheetSwitcher isOpen={isOpen} onClose={onClose}>
<ModalHeader
title={metadataName}
showBackButton
onBackButtonClicked={onBackButtonClicked}
{...(isDesktop ? {showCloseButton: true, onClose} : {})}
/>
<Content>
<div className="flex flex-col items-center space-y-1.5">
<AvatarDao daoName={metadataName} src={metadataIcon} size="medium" />
<div className="flex justify-center items-center font-bold text-center text-ui-800">
<Spinner size={'xs'} />
<p className="ml-2">{t('wc.detaildApp.spinnerLabel')}</p>
</div>
<p className="desktop:px-5 text-sm text-center text-ui-500">
{t('wc.detaildApp.desc', {
dappName: metadataName,
})}
</p>
{actionsReceived.length > 0 ? (
<Tag
label={t('wc.detaildApp.amountActionsTag', {
amountActions: actionsReceived.length,
})}
/>
) : (
<Tag label={t('wc.detaildApp.noActionsTag')} />
)}
</div>

<div className="space-y-1.5">
{actionsReceived.length > 0 ? (
<ButtonText
label={t('wc.detaildApp.ctaLabel.addAmountActions', {
amountActions: actionsReceived.length,
})}
onClick={handleAddActions}
mode="primary"
className="w-full"
/>
) : null}
<ButtonText
label={t('wc.detaildApp.ctaLabel.opendApp', {
dappName: metadataName,
})}
onClick={() => window.open(metadataURL, '_blank')}
mode="ghost"
bgWhite
className="w-full"
/>
<ButtonText
label={t('wc.detaildApp.ctaLabel.disconnectdApp', {
dappName: metadataName,
})}
onClick={async () => {
await wcDisconnect(selectedSession.topic);
onBackButtonClicked();
}}
mode="ghost"
className="w-full"
/>
</div>
</Content>
</ModalBottomSheetSwitcher>
);
};

export default ActionListenerModal;

const Content = styled.div.attrs({
className: 'py-3 px-2 desktop:px-3 space-y-3',
})``;
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, {useState} from 'react';
import styled from 'styled-components';
import {SessionTypes} from '@walletconnect/types';
import {
ButtonText,
IconChevronRight,
IconFeedback,
IconReload,
IconSearch,
Link,
ListItemAction,
} from '@aragon/ui-components';
import {useTranslation} from 'react-i18next';

import ModalBottomSheetSwitcher from 'components/modalBottomSheetSwitcher';
import Header from 'containers/smartContractComposer/desktopModal/header';
import {StateEmpty} from 'components/stateEmpty';

type Props = {
sessions: Record<string, SessionTypes.Struct> | undefined;
onConnectNewdApp: () => void;
onSelectExistingdApp: (session: SessionTypes.Struct) => void;
onClose: () => void;
isOpen: boolean;
};

const WCConnectedApps: React.FC<Props> = props => {
const [search, setSearch] = useState('');
const {t} = useTranslation();

let filteredSessions: Array<SessionTypes.Struct> = [];

if (props.sessions) {
filteredSessions = Object.values(props.sessions).filter(session =>
session.peer.metadata.name.toLowerCase().includes(search.toLowerCase())
);
}

const handleResetSearch = () => setSearch('');
/*************************************************
* Render *
*************************************************/
return (
<ModalBottomSheetSwitcher isOpen={props.isOpen} onClose={props.onClose}>
<Header
onClose={props.onClose}
onSearch={setSearch}
buttonIcon={search ? <IconSearch /> : undefined}
/>
<Content>
{props.sessions !== undefined && filteredSessions.length === 0 ? (
<StateEmpty
mode="inline"
type="Object"
object="magnifying_glass"
title={t('wc.listdApps.emptyStateSearch.title')}
description={t('wc.listdApps.emptyStateSearch.desc')}
secondaryButton={{
label: t('wc.listdApps.emptyStateSearch.ctaLabel'),
onClick: handleResetSearch,
iconLeft: <IconReload />,
className: 'w-full',
bgWhite: false,
}}
/>
) : (
<>
<div className="space-y-1">
<p className="text-sm font-bold text-ui-400">
{search
? t('wc.listdApps.pretitle.searchResults', {
amount: filteredSessions.length,
})
: t('wc.listdApps.listTitle', {
amount: filteredSessions.length,
})}
</p>
{filteredSessions.map(session => (
<ListItemAction
key={session.topic}
title={session.peer.metadata.name}
subtitle={session.peer.metadata.description}
iconLeft={session.peer.metadata.icons[0]}
bgWhite
iconRight={<IconChevronRight />}
truncateText
onClick={() => props.onSelectExistingdApp(session)}
/>
))}
</div>
<ButtonText
mode="secondary"
size="large"
label={t('wc.listdApps.ctaLabelDefault')}
onClick={() => {
props.onConnectNewdApp();
}}
className="mt-3 w-full"
/>
<div className="mt-2 text-center">
<Link
label={t('wc.listdApps.learnLinkLabel')}
href="/"
external
iconRight={<IconFeedback />}
/>
</div>
</>
)}
</Content>
</ModalBottomSheetSwitcher>
);
};

export default WCConnectedApps;

const Content = styled.div.attrs({
className: 'py-3 px-2 desktop:px-3',
})``;
Loading

0 comments on commit 0ba64fa

Please sign in to comment.