Skip to content

Commit

Permalink
Merge pull request #1356 from blockscout/marketplace-improvements
Browse files Browse the repository at this point in the history
Marketplace improvements
  • Loading branch information
isstuev authored Dec 14, 2023
2 parents 84ed9b9 + 68fb687 commit 2d7ce28
Show file tree
Hide file tree
Showing 53 changed files with 1,215 additions and 149 deletions.
3 changes: 3 additions & 0 deletions icons/profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Type extends EventTypes.VERIFY_TOKEN ? {
'Action': 'Form opened' | 'Submit';
} :
Type extends EventTypes.WALLET_CONNECT ? {
'Source': 'Header' | 'Smart contracts';
'Status': 'Started' | 'Connected';
} :
Type extends EventTypes.CONTRACT_INTERACTION ? {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"chakra-react-select": "^4.4.3",
"crypto-js": "^4.1.1",
"d3": "^7.6.1",
"dappscout-iframe": "^0.1.0",
"dayjs": "^1.11.5",
"dom-to-image": "^2.6.0",
"framer-motion": "^6.5.1",
Expand Down
25 changes: 14 additions & 11 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import theme from 'theme';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';

import 'lib/setLocale';

Expand Down Expand Up @@ -52,17 +53,19 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
{ ...ERROR_SCREEN_STYLES }
onError={ handleError }
>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
<Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
{ getLayout(<Component { ...pageProps }/>) }
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools buttonPosition="bottom-left" position="left"/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
</Web3ModalProvider>
</AppErrorBoundary>
</ChakraProvider>
);
Expand Down
10 changes: 10 additions & 0 deletions pages/apps/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { NextPageWithLayout } from 'nextjs/types';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';

import LayoutApp from 'ui/shared/layout/LayoutApp';

const MarketplaceApp = dynamic(() => import('ui/pages/MarketplaceApp'), { ssr: false });

const Page: NextPageWithLayout<Props> = (props: Props) => {
Expand All @@ -16,6 +18,14 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
);
};

Page.getLayout = function getLayout(page: React.ReactElement) {
return (
<LayoutApp>
{ page }
</LayoutApp>
);
};

export default Page;

export { marketplace as getServerSideProps } from 'nextjs/getServerSideProps';
2 changes: 1 addition & 1 deletion pages/apps/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Page: NextPage = () => {
return (
<PageNextJs pathname="/apps">
<>
<PageTitle title="Marketplace"/>
<PageTitle title="DAppscout"/>
<Marketplace/>
</>
</PageNextJs>
Expand Down
4 changes: 2 additions & 2 deletions ui/address/contract/ContractConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const ContractConnectWallet = () => {
setIsModalOpening(true);
await open();
setIsModalOpening(false);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Started' });
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Started' });
}, [ open ]);

const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => {
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Connected' });
!isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: 'Smart contracts', Status: 'Connected' });
}, []);

const handleDisconnect = React.useCallback(() => {
Expand Down
11 changes: 11 additions & 0 deletions ui/marketplace/MarketplaceAppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Props extends MarketplaceAppPreview {
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
showDisclaimer: (id: string) => void;
}

const MarketplaceAppCard = ({
Expand All @@ -30,9 +31,18 @@ const MarketplaceAppCard = ({
isFavorite,
onFavoriteClick,
isLoading,
showDisclaimer,
}: Props) => {
const categoriesLabel = categories.join(', ');

const handleClick = useCallback((event: MouseEvent) => {
const isShown = window.localStorage.getItem('marketplace-disclaimer-shown');
if (!isShown) {
event.preventDefault();
showDisclaimer(id);
}
}, [ showDisclaimer, id ]);

const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
onInfoClick(id);
Expand Down Expand Up @@ -100,6 +110,7 @@ const MarketplaceAppCard = ({
url={ url }
external={ external }
title={ title }
onClick={ handleClick }
/>
</Skeleton>

Expand Down
6 changes: 4 additions & 2 deletions ui/marketplace/MarketplaceAppCardLink.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { LinkOverlay } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import type { MouseEvent } from 'react';

type Props = {
id: string;
url: string;
external?: boolean;
title: string;
onClick?: (event: MouseEvent) => void;
}

const MarketplaceAppCardLink = ({ url, external, id, title }: Props) => {
const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => {
return external ? (
<LinkOverlay href={ url } isExternal={ true }>
{ title }
</LinkOverlay>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay>
<LinkOverlay onClick={ onClick }>
{ title }
</LinkOverlay>
</NextLink>
Expand Down
81 changes: 81 additions & 0 deletions ui/marketplace/MarketplaceDisclaimerModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Heading, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, Button, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';

import useIsMobile from 'lib/hooks/useIsMobile';

type Props = {
isOpen: boolean;
onClose: () => void;
appId: string;
}

const MarketplaceDisclaimerModal = ({ isOpen, onClose, appId }: Props) => {

const isMobile = useIsMobile();

const handleContinueClick = React.useCallback(() => {
window.localStorage.setItem('marketplace-disclaimer-shown', 'true');
}, [ ]);

return (
<Modal
isOpen={ isOpen }
onClose={ onClose }
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>

<ModalContent>
<ModalHeader>
<Heading
as="h2"
fontSize="2xl"
fontWeight="medium"
lineHeight={ 1 }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
>
Disclaimer
</Heading>
</ModalHeader>

<ModalBody>
<Text color={ useColorModeValue('gray.800', 'whiteAlpha.800') }>
You are now accessing a third-party app. Blockscout does not own, control, maintain, or audit 3rd party apps,{ ' ' }
and is not liable for any losses associated with these interactions. Please do so at your own risk.
<br/><br/>
By clicking continue, you agree that you understand the risks and have read the Disclaimer.
</Text>
</ModalBody>

<ModalFooter
display="flex"
flexDirection="row"
alignItems="center"
>
<NextLink href={{ pathname: '/apps/[id]', query: { id: appId } }} passHref legacyBehavior>
<Button
variant="solid"
colorScheme="blue"
mr={ 6 }
py="10px"
onClick={ handleContinueClick }
>
Continue to app
</Button>
</NextLink>
<Button
variant="outline"
colorScheme="blue"
onClick={ onClose }
>
Cancel
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

export default MarketplaceDisclaimerModal;
4 changes: 3 additions & 1 deletion ui/marketplace/MarketplaceList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ type Props = {
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
showDisclaimer: (id: string) => void;
}

const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading }: Props) => {
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading, showDisclaimer }: Props) => {
return apps.length > 0 ? (
<Grid
templateColumns={{
Expand All @@ -41,6 +42,7 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLo
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading }
showDisclaimer={ showDisclaimer }
/>
)) }
</Grid>
Expand Down
20 changes: 19 additions & 1 deletion ui/marketplace/useMarketplace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export default function useMarketplace() {
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);

const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
Expand All @@ -46,10 +48,20 @@ export default function useMarketplace() {

const showAppInfo = React.useCallback((id: string) => {
setSelectedAppId(id);
setIsAppInfoModalOpen(true);
}, []);

const showDisclaimer = React.useCallback((id: string) => {
setSelectedAppId(id);
setIsDisclaimerModalOpen(true);
}, []);

const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = React.useCallback(() => setSelectedAppId(null), []);
const clearSelectedAppId = React.useCallback(() => {
setSelectedAppId(null);
setIsAppInfoModalOpen(false);
setIsDisclaimerModalOpen(false);
}, []);

const handleCategoryChange = React.useCallback((newCategory: string) => {
setSelectedCategoryId(newCategory);
Expand Down Expand Up @@ -104,6 +116,9 @@ export default function useMarketplace() {
clearSelectedAppId,
favoriteApps,
onFavoriteClick: handleFavoriteClick,
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
}), [
selectedCategoryId,
categories,
Expand All @@ -118,5 +133,8 @@ export default function useMarketplace() {
isPlaceholderData,
showAppInfo,
debouncedFilterQuery,
isAppInfoModalOpen,
isDisclaimerModalOpen,
showDisclaimer,
]);
}
64 changes: 64 additions & 0 deletions ui/marketplace/useMarketplaceWallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { TypedData } from 'abitype';
import { useCallback } from 'react';
import type { Account, SignTypedDataParameters } from 'viem';
import { useAccount, useSendTransaction, useSwitchNetwork, useNetwork, useSignMessage, useSignTypedData } from 'wagmi';

import config from 'configs/app';

type SendTransactionArgs = {
chainId?: number;
mode?: 'prepared';
to: string;
};

export type SignTypedDataArgs<
TTypedData extends
| TypedData
| {
[key: string]: unknown;
} = TypedData,
TPrimaryType extends string = string,
> = SignTypedDataParameters<TTypedData, TPrimaryType, Account>;

export default function useMarketplaceWallet() {
const { address } = useAccount();
const { chain } = useNetwork();
const { sendTransactionAsync } = useSendTransaction();
const { signMessageAsync } = useSignMessage();
const { signTypedDataAsync } = useSignTypedData();
const { switchNetworkAsync } = useSwitchNetwork({ chainId: Number(config.chain.id) });

const switchNetwork = useCallback(async() => {
if (Number(config.chain.id) !== chain?.id) {
await switchNetworkAsync?.();
}
}, [ chain, switchNetworkAsync ]);

const sendTransaction = useCallback(async(transaction: SendTransactionArgs) => {
await switchNetwork();
const tx = await sendTransactionAsync(transaction);
return tx.hash;
}, [ sendTransactionAsync, switchNetwork ]);

const signMessage = useCallback(async(message: string) => {
await switchNetwork();
const signature = await signMessageAsync({ message });
return signature;
}, [ signMessageAsync, switchNetwork ]);

const signTypedData = useCallback(async(typedData: SignTypedDataArgs) => {
await switchNetwork();
if (typedData.domain) {
typedData.domain.chainId = Number(typedData.domain.chainId);
}
const signature = await signTypedDataAsync(typedData);
return signature;
}, [ signTypedDataAsync, switchNetwork ]);

return {
address,
sendTransaction,
signMessage,
signTypedData,
};
}
Loading

0 comments on commit 2d7ce28

Please sign in to comment.