Skip to content

Commit

Permalink
wallet connect 2 support
Browse files Browse the repository at this point in the history
  • Loading branch information
juliancwirko committed Mar 5, 2023
1 parent 59d08ad commit dac959e
Show file tree
Hide file tree
Showing 14 changed files with 2,936 additions and 452 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
# MultiversX chain (can be devnet, testnet, mainnet)
NEXT_PUBLIC_MULTIVERSX_CHAIN = devnet

# Wallet Connect 2 Project Id. This one will work only with this project
# Get yours at: https://cloud.walletconnect.com/sign-in
NEXT_PUBLIC_WC_PROJECT_ID = be161e9c2764269adc6a5cf4304c3a22

#
# Either the public API endpoint of your MultiversX api
# or the masked proxy that will be used instead.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### [7.0.0](https://github.com/xdevguild/nextjs-dapp-template/releases/tag/v7.0.0) (2023-03-05)
- switch to v0.1.0 of [useElven](https://www.useelven.com/) with support for xPortal when signing
- changes for Wallet Connect pairings list
- other minor improvements

### [6.1.0](https://github.com/xdevguild/nextjs-dapp-template/releases/tag/v6.1.0) (2023-03-04)
- fix passing custom configuration, one should use .env variables for that, `useNetworkSync` will read from them

Expand Down
27 changes: 14 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,18 @@ export default Profile;

### Working with the API

The API endpoint is proxied on the backend side. The only public API endpoint is `/api/multiversx`. This is useful when you don't want to show the API endpoint because, for example, you use the paid ones. Also, there is an option to block the `/api/multiversx` endpoint to be used only within the Dapp, even previewing it in the browser won't be possible.
By default, the Dapp provides the `.env.example`, configured not to use the API rewrites and configured to use the official public MultiversX API endpoint.

You can use `API_ALLOWED_DAPP_HOST` in the .env file to enable `/api/multiversx` restrictions. If you don't want to restrict it, you can remove that variable.
You have three options:

In the `middleware.ts`, you'll find the logic for the API restrictions. And in the `next.config.js`, you'll find the configuration for rewrites of the API.

In this demo, the Dapp uses a public API endpoint, so it isn't essential, but it is beneficial when you need to use paid 3rd party service.

Read more about it here: https://www.elven.tools/docs/dapp-api-proxy.html
1. By commenting this out the dapp will use the default MultiversX api endpoint (e.g. https://devnet-api.multiversx.com) \
**Note**: `MULTIVERSX_PRIVATE_API` needs to be removed/commented out.
2. Set this to an absolute address to use a custom MultiversX api endpoint
(e.g. http://dev.mydomain.com) \
**Note**: `MULTIVERSX_PRIVATE_API` needs to be removed/commented out.
3. Enter a relative path to proxy/mask your MultiversX api endpoint (e.g. /api/multiversx)
Only current instance of the Dapp can use it if only `API_ALLOWED_DAPP_HOST` is set. \
**Note**: `MULTIVERSX_PRIVATE_API` must include the actual MultiversX API endpoint.

### Working with the .env and config files

Expand All @@ -124,6 +127,10 @@ Here are all variables:
# MultiversX chain (can be devnet, testnet, mainnet)
NEXT_PUBLIC_MULTIVERSX_CHAIN = devnet

# Wallet Connect 2 Project Id. This one will work only with this project
# Get yours at: https://cloud.walletconnect.com/sign-in
NEXT_PUBLIC_WC_PROJECT_ID = be161e9c2764269adc6a5cf4304c3a22

#
# Either the public API endpoint of your MultiversX api
# or the masked proxy that will be used instead.
Expand Down Expand Up @@ -208,12 +215,6 @@ Read more about it here: https://www.elven.tools/docs/dapp-deployment.html

Here are other deployment solutions: [NextJS Deployment](https://nextjs.org/docs/deployment).

### Missing for now:

- More docs and examples
- More tooling and components
- tests

### Other solutions

The same dapp, but with Tailwind instead Chakra UI:
Expand Down
3 changes: 1 addition & 2 deletions components/demo/SimpleEGLDTxDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ export const SimpleEGLDTxDemo = ({
>
{shortenHash(egldTransferAddress, 8)}
</Link>{' '}
<br />
({chainType})
<br />({chainType})
</Text>
<ActionButton disabled={pending} onClick={handleSendTx}>
<Text>Send Transaction</Text>
Expand Down
3 changes: 1 addition & 2 deletions components/demo/SimpleNftMintDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ export const SimpleNftMintDemo = ({
>
{shortenHash(mintSmartContractAddress, 8)}
</Link>{' '}
<br />
({chainType}, max 10 NFTs per address)
<br />({chainType}, max 10 NFTs per address)
</Text>
<ActionButton disabled={pending} onClick={handleSendTx}>
<Text>Mint</Text>
Expand Down
3 changes: 1 addition & 2 deletions components/demo/SimpleScQueryDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ export const SimpleScQeryDemo = ({
>
{shortenHash(mintSmartContractAddress, 8)}
</Link>{' '}
<br />
({chainType})
<br />({chainType})
</Text>
<ActionButton disabled={isLoading || isValidating} onClick={fetch}>
<Text>Query</Text>
Expand Down
45 changes: 38 additions & 7 deletions components/tools/LoginComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@
import { useCallback, memo, useState } from 'react';
import { Box, Stack } from '@chakra-ui/react';
import { useLogin, LoginMethodsEnum } from '@useelven/core';
import { MobileLoginQR } from './MobileLoginQR';
import { WalletConnectQRCode } from './WalletConnectQRCode';
import { WalletConnectPairings } from './WalletConnectPairings';
import { ActionButton } from './ActionButton';
import { LedgerAccountsList } from './LedgerAccountsList';

export const LoginComponent = memo(() => {
// If you need the auth signature and token you can pass it here
// example: const { ... } = useLogin({ token: "some_hash_here" })
// If you need the auth signature and token pas your unique token in useLogin
// all auth providers will return the signature, it will be saved in localstorage and global state
const { login, isLoggedIn, error, walletConnectUri, getHWAccounts } =
useLogin();
// For the demo purposes here is a dummy token
const {
login,
isLoggedIn,
error,
walletConnectUri,
getHWAccounts,
walletConnectPairingLogin,
walletConnectPairings,
walletConnectRemovePairing,
setLoggingInState,
} = useLogin({ token: 'token_just_for_testing_purposes' });

const [loginMethod, setLoginMethod] = useState<LoginMethodsEnum>();

Expand All @@ -32,7 +42,19 @@ export const LoginComponent = memo(() => {
setLoginMethod(undefined);
}, []);

if (error) return <Box textAlign="center">{error}</Box>;
const backToOptions = useCallback(() => {
setLoggingInState('error', '');
}, [setLoggingInState]);

if (error)
return (
<Stack>
<Box textAlign="center">{error}</Box>
<ActionButton isFullWidth onClick={backToOptions}>
Back
</ActionButton>
</Stack>
);

return (
<>
Expand Down Expand Up @@ -65,9 +87,18 @@ export const LoginComponent = memo(() => {
</Stack>
{loginMethod === LoginMethodsEnum.walletconnect && walletConnectUri && (
<Box mt={5}>
<MobileLoginQR walletConnectUri={walletConnectUri} />
<WalletConnectQRCode uri={walletConnectUri} />
</Box>
)}
{loginMethod === LoginMethodsEnum.walletconnect &&
walletConnectPairings &&
walletConnectPairings.length > 0 && (
<WalletConnectPairings
pairings={walletConnectPairings}
login={walletConnectPairingLogin}
remove={walletConnectRemovePairing}
/>
)}
{loginMethod === LoginMethodsEnum.ledger && (
<LedgerAccountsList
getHWAccounts={getHWAccounts}
Expand Down
50 changes: 39 additions & 11 deletions components/tools/LoginModalButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import {
useDisclosure,
Spinner,
Flex,
ModalHeader,
Stack,
} from '@chakra-ui/react';
import { FC } from 'react';
import { useLogin, useLogout } from '@useelven/core';
import { useLogin, useLoginInfo, useLogout } from '@useelven/core';
import { ActionButton } from '../tools/ActionButton';
import { LoginComponent } from '../tools/LoginComponent';
import { useEffectOnlyOnUpdate } from '../../hooks/useEffectOnlyOnUpdate';
import { getLoginMethodDeviceName } from '../../utils/getSigningDeviceName';

interface LoginModalButtonProps {
onClose?: () => void;
Expand All @@ -28,7 +31,8 @@ export const LoginModalButton: FC<LoginModalButtonProps> = ({
onClose,
onOpen,
}) => {
const { isLoggedIn, isLoggingIn } = useLogin();
const { isLoggedIn, isLoggingIn, setLoggingInState } = useLogin();
const { loginMethod } = useLoginInfo();
const { logout } = useLogout();
const {
isOpen: opened,
Expand All @@ -42,6 +46,12 @@ export const LoginModalButton: FC<LoginModalButtonProps> = ({
}
}, [isLoggedIn]);

const onCloseComplete = () => {
setLoggingInState('error', '');
};

const ledgerOrPortalName = getLoginMethodDeviceName(loginMethod);

return (
<>
{isLoggedIn ? (
Expand All @@ -51,7 +61,14 @@ export const LoginModalButton: FC<LoginModalButtonProps> = ({
{isLoggingIn ? 'Connecting...' : 'Connect'}
</ActionButton>
)}
<Modal isOpen={opened} size="sm" onClose={close} isCentered>
<Modal
isOpen={opened}
size="sm"
onClose={close}
isCentered
scrollBehavior="inside"
onCloseComplete={onCloseComplete}
>
<CustomModalOverlay />
<ModalContent
bgColor="dappTemplate.dark.darker"
Expand All @@ -61,25 +78,36 @@ export const LoginModalButton: FC<LoginModalButtonProps> = ({
position="relative"
>
<ModalCloseButton _focus={{ outline: 'none' }} />
<ModalBody>
<Text textAlign="center" mb={7} fontWeight="black" fontSize="2xl">
<ModalHeader>
<Text textAlign="center" fontWeight="black" fontSize="2xl">
Connect your wallet
</Text>
</ModalHeader>
<ModalBody>
{isLoggingIn && (
<Flex
alignItems="center"
backdropFilter="blur(3px)"
bgColor="blackAlpha.700"
justifyContent="center"
position="absolute"
zIndex="overlay"
inset={0}
>
<Spinner
thickness="3px"
speed="0.4s"
color="dappTemplate.color2.base"
size="xl"
/>
<Stack alignItems="center">
{ledgerOrPortalName ? (
<>
<Text fontSize="lg">Confirmation required</Text>
<Text fontSize="sm">Approve on {ledgerOrPortalName}</Text>
</>
) : null}
<Spinner
thickness="3px"
speed="0.4s"
color="dappTemplate.color2.base"
size="xl"
/>
</Stack>
</Flex>
)}
<LoginComponent />
Expand Down
71 changes: 71 additions & 0 deletions components/tools/WalletConnectPairings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { FC, MouseEventHandler } from 'react';
import { PairingTypes } from '@useelven/core';
import { Stack, Box, Text, Heading, IconButton } from '@chakra-ui/react';
import { CloseIcon } from '@chakra-ui/icons';

interface WalletConnectPairingsProps {
pairings: PairingTypes.Struct[];
login: (topic: string) => Promise<void>;
remove: (topic: string) => Promise<void>;
}

export const WalletConnectPairings: FC<WalletConnectPairingsProps> = ({
pairings,
login,
remove,
}) => {
const handleLogin = (topic: string) => () => {
login(topic);
};

const handleRemove =
(topic: string): MouseEventHandler<HTMLButtonElement> | undefined =>
(e) => {
e.stopPropagation();
remove(topic);
};

return (
<Stack>
{pairings?.length > 0 && (
<Heading size="md" mt={4}>
Existing pairings:
</Heading>
)}
{pairings.map((pairing) => (
<Box
bgColor="dappTemplate.white"
py={2}
px={4}
pr={8}
borderRadius="md"
key={pairing.topic}
cursor="pointer"
onClick={handleLogin(pairing.topic)}
userSelect="none"
position="relative"
>
<IconButton
position="absolute"
top={2}
right={2}
aria-label="remove-pairing"
color="dappTemplate.dark.base"
h={6}
minW={6}
icon={<CloseIcon boxSize={2} />}
onClick={handleRemove(pairing.topic)}
/>
<Text fontSize="lg" color="dappTemplate.dark.base">
{pairing.peerMetadata?.name}
</Text>
{pairing.peerMetadata?.url ? (
<Text fontSize="xs" color="dappTemplate.dark.base">
({pairing.peerMetadata.url})
</Text>
) : null}
</Box>
))}
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ import { useConfig } from '@useelven/core';
import { isMobile } from '../../utils/isMobile';
import QRCode from 'qrcode';

interface MobileLoginQRProps {
walletConnectUri: string;
interface WalletConnectQRCodeProps {
uri: string;
}

export const MobileLoginQR: FunctionComponent<MobileLoginQRProps> = ({
walletConnectUri,
}) => {
export const WalletConnectQRCode: FunctionComponent<
WalletConnectQRCodeProps
> = ({ uri }) => {
const [qrCodeSvg, setQrCodeSvg] = useState('');
const { walletConnectDeepLink } = useConfig();

useEffect(() => {
const generateQRCode = async () => {
if (!walletConnectUri) {
if (!uri) {
return;
}

const svg = await QRCode.toString(walletConnectUri, {
const svg = await QRCode.toString(uri, {
type: 'svg',
});

setQrCodeSvg(svg);
};
generateQRCode();
}, [walletConnectUri]);
}, [uri]);

const mobile = isMobile();

Expand Down Expand Up @@ -60,7 +60,7 @@ export const MobileLoginQR: FunctionComponent<MobileLoginQRProps> = ({
transition="background-color .3s"
as="a"
href={`${walletConnectDeepLink}?wallet-connect=${encodeURIComponent(
walletConnectUri
uri
)}`}
rel="noopener noreferrer nofollow"
target="_blank"
Expand Down
Loading

0 comments on commit dac959e

Please sign in to comment.