feat(auth-wallet): add handling for push methods/events (#145)

Co-authored-by: Ben Kremer <ben@walletconnect.com>
This commit is contained in:
Cali 2023-05-31 15:13:42 +03:00 committed by GitHub
parent 2ff0874778
commit 34cf292b06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1291 additions and 81 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,12 +15,15 @@
"@nextui-org/react": "1.0.8-beta.5",
"@polkadot/keyring": "^10.1.2",
"@solana/web3.js": "1.43.0",
"@walletconnect/auth-client": "2.1.0",
"@walletconnect/utils": "2.7.6",
"@walletconnect/auth-client": "^2.1.0",
"@walletconnect/core": "^2.7.6",
"@walletconnect/push-client": "0.10.0",
"@walletconnect/utils": "^2.7.6",
"bs58": "5.0.0",
"cosmos-wallet": "1.2.0",
"ethers": "5.6.6",
"framer-motion": "6.3.3",
"lokijs": "^1.5.12",
"mnemonic-keyring": "1.4.0",
"next": "12.1.5",
"react": "17.0.2",
@ -33,7 +36,7 @@
"devDependencies": {
"@types/node": "17.0.35",
"@types/react": "18.0.9",
"@walletconnect/types": "2.7.6",
"@walletconnect/types": "^2.7.6",
"eslint": "8.15.0",
"eslint-config-next": "12.1.6",
"eslint-config-prettier": "8.5.0",

View File

@ -0,0 +1,10 @@
<svg width="15" height="23" viewBox="0 0 15 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.0001 0C6.07054 0 3.52589 2.0154 2.8549 4.86708L0.578472 14.5419C0.283081 15.7973 1.23561 17 2.52531 17H15.4749C16.7646 17 17.7171 15.7973 17.4217 14.5419L15.1453 4.86709C14.4743 2.0154 11.9297 0 9.0001 0Z" fill="url(#gradient)" />
<path d="M12.7231 19.9655C13.0268 19.1947 12.3285 15.5 11.5001 15.5H6.5001C5.67167 15.5 4.97344 19.1947 5.27708 19.9655C5.86177 21.4497 7.30827 22.5 9.0001 22.5C10.6919 22.5 12.1384 21.4497 12.7231 19.9655Z" fill="url(#gradient" />
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="0" y2="100%">
<stop offset="0%" stop-color="#A562D5" />
<stop offset="100%" stop-color="#306FEB" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 786 B

View File

@ -1,5 +1,6 @@
import ModalStore from '@/store/ModalStore'
import AuthenticationRequestModal from '@/views/AuthenticationRequestModal'
import PushRequestModal from '@/views/PushRequestModal'
import { Modal as NextModal } from '@nextui-org/react'
import { useSnapshot } from 'valtio'
@ -9,6 +10,7 @@ export default function Modal() {
return (
<NextModal blur open={open} style={{ border: '1px solid rgba(139, 139, 139, 0.4)' }}>
{view === 'AuthenticationRequest' && <AuthenticationRequestModal />}
{view === 'PushRequest' && <PushRequestModal />}
</NextModal>
)
}

View File

@ -23,6 +23,17 @@ export default function Navigation() {
</a>
</Link>
<Link href="/notifications" passHref>
<a className="navLink">
<Image
alt="notifications icon"
src="/icons/notification-icon.svg"
width={25}
height={25}
/>
</a>
</Link>
<Link href="/walletconnect" passHref>
<a className="navLink">
<Avatar

View File

@ -0,0 +1,75 @@
import { truncate } from '@/utils/HelperUtil'
import { Avatar, Button, Card, Link, Text, Tooltip } from '@nextui-org/react'
import Image from 'next/image'
/**
* Types
*/
interface IProps {
logo?: string
title?: string
body?: string
url?: string
publishedAt?: number
onDelete: () => Promise<void>
}
/**
* Component
*/
export default function NotificationItem({
logo,
title,
body,
url,
publishedAt,
onDelete
}: IProps) {
return (
<Card
bordered
borderWeight="light"
css={{
position: 'relative',
marginBottom: '$6',
minHeight: '70px'
}}
>
<Card.Body
css={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
overflow: 'hidden'
}}
>
<Avatar src={logo} />
<div style={{ flex: 1 }}>
<Text h5 css={{ marginLeft: '$9' }}>
{title}
</Text>
{/* <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
</div> */}
<Text h6 css={{ marginLeft: '$9' }}>
{body}
</Text>
{publishedAt && (
<Text span css={{ marginLeft: '$9', fontSize: '12px' }}>
{new Date(publishedAt).toLocaleDateString()} -{' '}
{new Date(publishedAt).toLocaleTimeString()}
</Text>
)}
<Link href={url} css={{ marginLeft: '$9' }} target="_blank" rel="noopener noreferrer">
{url}
</Link>
</div>
<Tooltip content="Delete" placement="left">
<Button size="sm" color="error" flat onClick={onDelete} css={{ minWidth: 'auto' }}>
<Image src={'/icons/delete-icon.svg'} width={15} height={15} alt="delete icon" />
</Button>
</Tooltip>
</Card.Body>
</Card>
)
}

View File

@ -1,6 +1,6 @@
import SettingsStore from '@/store/SettingsStore'
import { createOrRestoreEIP155Wallet } from '@/utils/EIP155WalletUtil'
import { createAuthClient } from '@/utils/WalletConnectUtil'
import { createAuthClient, createPushClient } from '@/utils/WalletConnectUtil'
import { useCallback, useEffect, useState } from 'react'
export default function useInitialization() {
@ -11,6 +11,7 @@ export default function useInitialization() {
const { eip155Addresses } = createOrRestoreEIP155Wallet()
SettingsStore.setEIP155Address(eip155Addresses[0])
await createAuthClient()
await createPushClient()
setInitialized(true)
} catch (err: unknown) {
alert(err)

View File

@ -1,7 +1,6 @@
import { EIP155_SIGNING_METHODS } from '@/data/EIP155Data'
import ModalStore from '@/store/ModalStore'
import { authClient } from '@/utils/WalletConnectUtil'
import { useCallback, useEffect } from 'react'
import { authClient, pushClient } from '@/utils/WalletConnectUtil'
import { useEffect } from 'react'
export default function useWalletConnectEventsManager(initialized: boolean) {
/******************************************************************************
@ -9,6 +8,7 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
*****************************************************************************/
useEffect(() => {
if (initialized) {
// Auth client events
authClient.on('auth_request', ({ id, params }) => {
console.log('auth_request', { id, params })
ModalStore.open('AuthenticationRequest', {
@ -19,5 +19,19 @@ export default function useWalletConnectEventsManager(initialized: boolean) {
})
})
}
if (pushClient) {
// Push client events
pushClient.on('push_proposal', async ({ id, topic, params }) => {
console.log('push_proposal', { id, topic, params })
ModalStore.open('PushRequest', {
pushRequest: {
id,
topic,
params
}
})
})
}
}, [initialized])
}

View File

@ -0,0 +1,103 @@
import NotificationItem from '@/components/NotificationItem'
import PageHeader from '@/components/PageHeader'
import { getAndFormatNotifications, pushClient } from '@/utils/WalletConnectUtil'
import { Text, Collapse, Grid, Avatar, Button } from '@nextui-org/react'
import { PushClientTypes } from '@walletconnect/push-client'
import Image from 'next/image'
import { Fragment, useCallback, useEffect, useState } from 'react'
export default function NotificationsPage() {
const [notifications, setNotifications] = useState<
{
subscription: PushClientTypes.PushSubscription
messages: PushClientTypes.PushMessageRecord[]
}[]
>([])
const handleDeleteNotification = useCallback(async (messageId: number) => {
pushClient.deletePushMessage({ id: messageId })
const formattedNotifications = getAndFormatNotifications()
setNotifications(formattedNotifications)
}, [])
const handleDeleteSubscription = useCallback(async (topic: string) => {
await pushClient.deleteSubscription({ topic })
const formattedNotifications = getAndFormatNotifications()
setNotifications(formattedNotifications)
}, [])
useEffect(() => {
pushClient.on('push_message', () => {
const formattedNotifications = getAndFormatNotifications()
setNotifications(formattedNotifications)
})
}, [])
// Get notifications for the initial state
useEffect(() => {
const formattedNotifications = getAndFormatNotifications()
setNotifications(formattedNotifications)
}, [])
return (
<Fragment>
<PageHeader title="Notifications" />
<Grid.Container gap={2}>
<Grid>
<Collapse.Group shadow>
{notifications.map(notification => (
<Collapse
key={notification.subscription.topic}
title={<Text h4>{notification.subscription.metadata.name}</Text>}
subtitle={notification.subscription.metadata.description}
contentLeft={
<Avatar
size="lg"
src={notification.subscription.metadata.icons[0]}
color="secondary"
bordered
squared
/>
}
>
<Button
bordered
css={{
width: '100%',
marginBottom: '$6'
}}
color="error"
auto
onClick={() => handleDeleteSubscription(notification.subscription.topic)}
>
<Image src={'/icons/delete-icon.svg'} width={15} height={15} alt="delete icon" />
Delete subscription
</Button>
{notification.messages.length ? (
notification.messages
.sort((a, b) => b.publishedAt - a.publishedAt)
.map(({ id, message, publishedAt }) => (
<NotificationItem
key={id}
logo={message.icon}
url={message.url}
title={message.title}
body={message.body}
publishedAt={publishedAt}
onDelete={() => handleDeleteNotification(id)}
/>
))
) : (
<Text css={{ opacity: '0.5', textAlign: 'center', marginTop: '$6' }}>
No notifications
</Text>
)}
</Collapse>
))}
</Collapse.Group>
</Grid>
</Grid.Container>
</Fragment>
)
}

View File

@ -1,16 +1,16 @@
import { SessionTypes, SignClientTypes } from '@walletconnect/types'
import { proxy } from 'valtio'
/**
* Types
*/
interface ModalData {
authenticationRequest: any
authenticationRequest?: any
pushRequest?: any
}
interface State {
open: boolean
view?: 'AuthenticationRequest'
view?: 'AuthenticationRequest' | 'PushRequest'
data?: ModalData
}

View File

@ -1,12 +1,20 @@
import AuthClient from '@walletconnect/auth-client'
import pkg from '@walletconnect/auth-client/package.json'
import { Core } from '@walletconnect/core'
import { WalletClient } from '@walletconnect/push-client'
const core = new Core({
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!
})
console.log(`AuthClient@${pkg.version}`)
export let authClient: AuthClient
export let pushClient: WalletClient
export async function createAuthClient() {
authClient = await AuthClient.init({
core,
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
relayUrl: process.env.NEXT_PUBLIC_RELAY_URL || 'wss://relay.walletconnect.com',
metadata: {
@ -16,4 +24,36 @@ export async function createAuthClient() {
icons: ['https://avatars.githubusercontent.com/u/37784886']
}
})
const authClientId = await authClient.core.crypto.getClientId()
console.log({ authClientId })
}
export async function createPushClient() {
pushClient = await WalletClient.init({
logger: 'debug',
core,
projectId: process.env.NEXT_PUBLIC_PROJECT_ID!,
relayUrl: process.env.NEXT_PUBLIC_RELAY_URL || 'wss://relay.walletconnect.com'
})
const pushClientId = await pushClient.core.crypto.getClientId()
console.log({ pushClientId })
}
export const getAndFormatNotifications = () => {
if (!pushClient) {
return []
}
const activeSubscriptions = pushClient.getActiveSubscriptions()
const allMessagesWithSubscription = Object.entries(activeSubscriptions)
.map(([topic, subscription]) => ({
subscription,
messages: Object.values(
pushClient.getMessageHistory({
topic
})
)
}))
.reverse()
return allMessagesWithSubscription
}

View File

@ -0,0 +1,71 @@
import { Fragment, useCallback, useEffect, useState } from 'react'
import RequestModalContainer from '@/components/RequestModalContainer'
import ModalStore from '@/store/ModalStore'
import { Button, Col, Link, Modal, Row, Text } from '@nextui-org/react'
import { createOrRestoreEIP155Wallet } from '@/utils/EIP155WalletUtil'
import { pushClient } from '@/utils/WalletConnectUtil'
import { getWalletAddressFromParams } from '@/utils/HelperUtil'
export default function PushRequestModal() {
const pushRequest = ModalStore.state.data?.pushRequest
const { params, id } = pushRequest
const [iss, setIss] = useState<string>()
const { eip155Wallets, eip155Addresses } = createOrRestoreEIP155Wallet()
useEffect(() => {
const iss = `did:pkh:${pushRequest.params.account}`
setIss(iss)
}, [pushRequest.params.account, eip155Addresses])
const onSign = useCallback(
async (message: string) => {
const wallet = eip155Wallets[getWalletAddressFromParams(eip155Addresses, params.account)]
const signature = await wallet.signMessage(message)
return signature
},
[eip155Wallets, eip155Addresses, params.account]
)
const onApprove = useCallback(async () => {
if (pushRequest && iss) {
await pushClient.approve({ id, onSign })
ModalStore.close()
}
}, [pushRequest, id, iss, onSign])
if (!pushRequest) {
return <Text>Missing push request</Text>
}
// Handle reject action
async function onReject() {
await pushClient.reject({ id, reason: 'User rejected push subscription request' })
ModalStore.close()
}
return (
<Fragment>
<RequestModalContainer title="Push Request">
<Row>
<Col>
<Text h5>Message</Text>
<Text style={{ whiteSpace: 'pre-wrap' }} color="$gray400">
Subscribe to{' '}
<Link href={params.metadata.url} target="_blank" rel="noopener noreferrer">
{params.metadata.url}
</Link>
</Text>
</Col>
</Row>
</RequestModalContainer>
<Modal.Footer>
<Button auto flat color="error" onClick={onReject}>
Reject
</Button>
<Button auto flat color="success" onClick={onApprove}>
Approve
</Button>
</Modal.Footer>
</Fragment>
)
}