Skip to content

Commit 7452618

Browse files
committed
feat: add Web3Mail and Web3Telegram support and add contacts page
1 parent c7c2f73 commit 7452618

File tree

10 files changed

+1729
-138
lines changed

10 files changed

+1729
-138
lines changed

package-lock.json

Lines changed: 1429 additions & 124 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
"@fontsource/space-mono": "^5.2.5",
2525
"@graphql-codegen/introspection": "^4.0.3",
2626
"@iexec/dataprotector": "^2.0.0-beta.12",
27+
"@iexec/web3mail": "^1.2.0",
28+
"@iexec/web3telegram": "^0.0.2-alpha",
2729
"@radix-ui/react-dialog": "^1.1.6",
2830
"@radix-ui/react-slot": "^1.1.2",
2931
"@radix-ui/react-toast": "^1.2.6",

src/components/NavBar/navigationItems.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { MessageSquare } from 'react-feather';
2+
13
// navigationItems.js
24
export const navigationItems = [
35
{
@@ -42,4 +44,9 @@ export const navigationItems = [
4244
</svg>
4345
),
4446
},
47+
{
48+
name: 'Send Messages',
49+
path: '/contacts',
50+
icon: <MessageSquare />,
51+
},
4552
];

src/config/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ export const ITEMS_PER_PAGE = 6;
99
*/
1010
export const WEB3MAIL_IDAPPS_WHITELIST_SC =
1111
'0x781482c39cce25546583eac4957fb7bf04c277d2';
12+
export const WEB3TELEGRAM_IDAPPS_WHITELIST_SC =
13+
'0x192C6f5AccE52c81Fcc2670f10611a3665AAA98F';

src/externals/iexecSdkClient.ts

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import {
33
IExecDataProtectorCore,
44
IExecDataProtectorSharing,
55
} from '@iexec/dataprotector';
6+
import { IExecWeb3mail } from '@iexec/web3mail';
7+
import { IExecWeb3telegram } from '@iexec/web3telegram';
68
import { IExec, IExecConfig } from 'iexec';
79
import { type Connector } from 'wagmi';
810

911
let iExecDataProtectorCore: IExecDataProtectorCore | null = null;
1012
let iExecDataProtectorSharing: IExecDataProtectorSharing | null = null;
13+
let iExecWeb3mail: IExecWeb3mail | null = null;
14+
let iExecWeb3telegram: IExecWeb3telegram | null = null;
1115
let iExec: IExec | null = null;
1216

1317
// Basic promise queue for pending getDataProtectorCoreClient() requests
@@ -20,11 +24,17 @@ const DATA_PROTECTOR_SHARING_CLIENT_RESOLVES: Array<
2024
> = [];
2125
// Basic promise queue for pending getIExec() requests
2226
const IEXEC_CLIENT_RESOLVES: Array<Promise<IExec>> = [];
27+
// Basic promise queue for pending getWeb3mailClient() requests
28+
const WEB3MAIL_CLIENT_RESOLVES: Array<Promise<IExecWeb3mail>> = [];
29+
// Basic promise queue for pending getWeb3telegramClient() requests
30+
const WEB3TELEGRAM_CLIENT_RESOLVES: Array<Promise<IExecWeb3telegram>> = [];
2331

2432
// Clean both SDKs
2533
export function cleanIExecSDKs() {
2634
iExecDataProtectorCore = null;
2735
iExecDataProtectorSharing = null;
36+
iExecWeb3mail = null;
37+
iExecWeb3telegram = null;
2838
iExec = null;
2939
}
3040

@@ -40,11 +50,7 @@ export async function initIExecSDKs({ connector }: { connector?: Connector }) {
4050
return;
4151
}
4252

43-
const dataProtectorParent = new IExecDataProtector(provider, {
44-
iexecOptions: {
45-
smsURL: 'https://sms.scone-debug.v8-bellecour.iex.ec/',
46-
},
47-
});
53+
const dataProtectorParent = new IExecDataProtector(provider);
4854

4955
iExecDataProtectorCore = dataProtectorParent.core;
5056
iExecDataProtectorSharing = dataProtectorParent.sharing;
@@ -67,6 +73,18 @@ export async function initIExecSDKs({ connector }: { connector?: Connector }) {
6773
return resolve(iExec);
6874
});
6975
IEXEC_CLIENT_RESOLVES.length = 0;
76+
77+
iExecWeb3mail = new IExecWeb3mail(provider);
78+
WEB3MAIL_CLIENT_RESOLVES.forEach((resolve) => {
79+
return resolve(iExecWeb3mail);
80+
});
81+
WEB3MAIL_CLIENT_RESOLVES.length = 0;
82+
83+
iExecWeb3telegram = new IExecWeb3telegram(provider);
84+
WEB3TELEGRAM_CLIENT_RESOLVES.forEach((resolve) => {
85+
return resolve(iExecWeb3telegram);
86+
});
87+
WEB3TELEGRAM_CLIENT_RESOLVES.length = 0;
7088
}
7189

7290
export async function getDataProtectorCoreClient(): Promise<IExecDataProtectorCore> {
@@ -78,13 +96,18 @@ export async function getDataProtectorCoreClient(): Promise<IExecDataProtectorCo
7896
return iExecDataProtectorCore;
7997
}
8098

81-
export async function getDataProtectorSharingClient(): Promise<IExecDataProtectorSharing> {
82-
if (!iExecDataProtectorSharing) {
83-
return new Promise((resolve) =>
84-
DATA_PROTECTOR_SHARING_CLIENT_RESOLVES.push(resolve)
85-
);
99+
export async function getWeb3telegramClient(): Promise<IExecWeb3telegram> {
100+
if (!iExecWeb3telegram) {
101+
return new Promise((resolve) => WEB3TELEGRAM_CLIENT_RESOLVES.push(resolve));
102+
}
103+
return iExecWeb3telegram;
104+
}
105+
106+
export async function getWeb3mailClient(): Promise<IExecWeb3mail> {
107+
if (!iExecWeb3mail) {
108+
return new Promise((resolve) => WEB3MAIL_CLIENT_RESOLVES.push(resolve));
86109
}
87-
return iExecDataProtectorSharing;
110+
return iExecWeb3mail;
88111
}
89112

90113
export function getIExec(): Promise<IExec> {

src/router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createBrowserRouter, Navigate } from 'react-router-dom';
22
import MainLayout from './layouts/MainLayout.tsx';
3+
import ContactList from './views/contact/contactList.tsx';
34
import AddProtectedData from './views/myData/addProtectedData.tsx';
45
import ProtectedData from './views/myData/protectedData.tsx';
56
import ProtectedDataList from './views/myData/protectedDataList.tsx';
@@ -24,6 +25,10 @@ export const router = createBrowserRouter([
2425
path: '/my-data/:protectedDataAddress',
2526
element: <ProtectedData />,
2627
},
28+
{
29+
path: '/contacts',
30+
element: <ContactList />,
31+
},
2732
],
2833
},
2934
]);

src/views/contact/contactList.tsx

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import {
2+
ITEMS_PER_PAGE,
3+
WEB3MAIL_IDAPPS_WHITELIST_SC,
4+
WEB3TELEGRAM_IDAPPS_WHITELIST_SC,
5+
} from '@/config/config';
6+
import { Address } from '@/types';
7+
import { ProtectedData } from '@iexec/dataprotector';
8+
import { useMutation, useQuery } from '@tanstack/react-query';
9+
import { useState } from 'react';
10+
import { NavLink } from 'react-router-dom';
11+
import { Alert } from '@/components/Alert';
12+
import { CircularLoader } from '@/components/CircularLoader';
13+
import { PaginatedNavigation } from '@/components/PaginatedNavigation';
14+
import { Button } from '@/components/ui/button';
15+
import {
16+
getDataProtectorCoreClient,
17+
getWeb3mailClient,
18+
getWeb3telegramClient,
19+
} from '@/externals/iexecSdkClient';
20+
import useUserStore from '@/stores/useUser.store';
21+
import { chunkArray } from '@/utils/chunkArray';
22+
import { cn } from '@/utils/style.utils';
23+
24+
const COLOR_CLASSES: {
25+
[key: string]: {
26+
gradientTo?: string;
27+
chip: string;
28+
};
29+
} = {
30+
all: {
31+
chip: 'before:bg-[#312a1e] bg-yellow-300 text-yellow-300',
32+
},
33+
mail: {
34+
gradientTo: 'to-[#50366b]',
35+
chip: 'before:bg-[#251E31] bg-[#BC70FD] text-[#BC70FD]',
36+
},
37+
telegram: {
38+
gradientTo: 'to-[#00115C]',
39+
chip: 'before:bg-[#161a2a] bg-[#728CFF] text-[#728CFF]',
40+
},
41+
};
42+
43+
export default function ContactList() {
44+
const { address: userAddress } = useUserStore();
45+
const [currentPage, setCurrentPage] = useState(0);
46+
const [selectedTab, setSelectedTab] = useState<'all' | 'telegram' | 'mail'>(
47+
'all'
48+
);
49+
50+
const contactList = useQuery({
51+
queryKey: ['fetchContact', userAddress],
52+
queryFn: async () => {
53+
if (!userAddress) {
54+
throw new Error('User address is undefined');
55+
}
56+
57+
const web3mail = await getWeb3mailClient();
58+
const myEmailContacts = await web3mail.fetchMyContacts({
59+
isUserStrict: true,
60+
});
61+
62+
const web3telegram = await getWeb3telegramClient();
63+
const myTelegramContacts = await web3telegram.fetchMyContacts();
64+
65+
const mergedContacts = [...myEmailContacts, ...myTelegramContacts];
66+
67+
const sortedContacts = mergedContacts.sort(
68+
(a, b) =>
69+
new Date(a.accessGrantTimestamp).getTime() -
70+
new Date(b.accessGrantTimestamp).getTime()
71+
);
72+
73+
const contactsWithProtectedDataAndGrantedAccess = await Promise.all(
74+
sortedContacts.map(async (contact) => {
75+
const contactProtectedData = await oneProtectedData.mutateAsync(
76+
contact.address as Address
77+
);
78+
const contactGrantedAccess =
79+
await oneGrantedAccess.mutateAsync(contactProtectedData);
80+
return {
81+
...contact,
82+
protectedData: contactProtectedData,
83+
grantedAccess: contactGrantedAccess,
84+
};
85+
})
86+
);
87+
return contactsWithProtectedDataAndGrantedAccess;
88+
},
89+
enabled: !!userAddress,
90+
refetchOnWindowFocus: true,
91+
});
92+
93+
const oneProtectedData = useMutation({
94+
mutationFn: async (protectedDataAddress: Address) => {
95+
const dataProtectorCore = await getDataProtectorCoreClient();
96+
const oneProtectedData = await dataProtectorCore.getProtectedData({
97+
protectedDataAddress: protectedDataAddress,
98+
});
99+
return oneProtectedData[0];
100+
},
101+
});
102+
103+
const oneGrantedAccess = useMutation({
104+
mutationFn: async (protectedData: ProtectedData) => {
105+
const dataProtectorCore = await getDataProtectorCoreClient();
106+
const oneGrantedAccess = await dataProtectorCore.getGrantedAccess({
107+
protectedData: protectedData.address,
108+
authorizedUser: userAddress,
109+
authorizedApp:
110+
getDataType(protectedData.schema) === 'mail'
111+
? WEB3MAIL_IDAPPS_WHITELIST_SC
112+
: WEB3TELEGRAM_IDAPPS_WHITELIST_SC,
113+
});
114+
return oneGrantedAccess.grantedAccess[0];
115+
},
116+
});
117+
118+
const getDataType = (schema: { [key: string]: unknown }) => {
119+
if (schema.email) {
120+
return 'mail';
121+
}
122+
if (schema.telegram_chatId) {
123+
return 'telegram';
124+
}
125+
};
126+
127+
const getProtectedDataByType = (type: 'all' | 'telegram' | 'mail') => {
128+
return (
129+
contactList.data?.filter((contact) =>
130+
type === 'all'
131+
? ['telegram', 'mail'].includes(
132+
getDataType(contact.protectedData.schema) || ''
133+
)
134+
: getDataType(contact.protectedData.schema) === type
135+
) || []
136+
);
137+
};
138+
139+
const pagesOfContacts =
140+
contactList &&
141+
chunkArray(getProtectedDataByType(selectedTab) || [], ITEMS_PER_PAGE);
142+
143+
return (
144+
<div className="space-y-6">
145+
<div className="flex items-center justify-between gap-2">
146+
<div>
147+
<h1 className="text-xl font-bold">Send Message to contact</h1>
148+
<p>Email or telegram contact info</p>
149+
</div>
150+
<Button>Add new contact TODO</Button>
151+
</div>
152+
<div className="flex flex-wrap gap-x-6 gap-y-3">
153+
{Object.keys(COLOR_CLASSES).map((key) => {
154+
return (
155+
<Button
156+
key={key}
157+
variant="chip"
158+
className={cn(
159+
'text-sm font-medium whitespace-nowrap',
160+
selectedTab === key && COLOR_CLASSES[key].chip
161+
)}
162+
onClick={() => {
163+
setSelectedTab(key as 'all' | 'telegram' | 'mail');
164+
if (currentPage !== 0) {
165+
setCurrentPage(0);
166+
}
167+
}}
168+
>
169+
{key.toUpperCase()}
170+
</Button>
171+
);
172+
})}
173+
</div>
174+
<div className="border-grey-600 my-5 grid w-full grid-cols-[2fr_2fr_2fr_2fr_2fr_1fr] items-center overflow-hidden rounded-3xl border [&>div]:px-5 [&>div]:py-5">
175+
<div className="text-grey-300 text-xs font-semibold">Name</div>
176+
<div className="text-grey-300 text-xs font-semibold">
177+
Protected data address
178+
</div>
179+
<div className="text-grey-300 text-xs font-semibold">Owner address</div>
180+
<div className="text-grey-300 text-xs font-semibold">
181+
Remaining access
182+
</div>
183+
<div className="text-grey-300 col-span-2 text-xs font-semibold">
184+
TYPE (Telegram/Mail)
185+
</div>
186+
{!pagesOfContacts || pagesOfContacts?.length === 0 ? (
187+
<div className="text-text-2 border-grey-600 col-span-6 flex h-48 items-center justify-center border-t text-center">
188+
{contactList.isLoading ? (
189+
<CircularLoader />
190+
) : contactList.isError ? (
191+
<Alert variant="error">
192+
<p>Oops, something went wrong while fetching contact list.</p>
193+
<p>{contactList.error.toString()}</p>
194+
</Alert>
195+
) : (
196+
<p>There are no contact yet.</p>
197+
)}
198+
</div>
199+
) : (
200+
pagesOfContacts[currentPage].map((contact) => {
201+
return (
202+
<div
203+
key={contact.address}
204+
className={cn(
205+
'bg-grey-50 even:*:bg-grey-800 *:border-grey-600 contents text-sm *:flex *:h-full *:items-center *:border-t *:px-5 *:py-3 odd:[a]:bg-red-300'
206+
)}
207+
>
208+
<div className="truncate">
209+
{contact.protectedData.name
210+
? contact.protectedData.name
211+
: '(No name)'}
212+
</div>
213+
<div className="truncate">
214+
<span className="truncate whitespace-nowrap">
215+
{contact.address}
216+
</span>
217+
</div>
218+
<div className="truncate">
219+
<span className="truncate whitespace-nowrap">
220+
{contact.owner === userAddress ? 'Me' : contact.owner}
221+
</span>
222+
</div>
223+
<div className="truncate">
224+
{contact.grantedAccess && contact.grantedAccess.volume}
225+
</div>
226+
<div className="text-primary truncate uppercase">
227+
{getDataType(contact.protectedData.schema)}
228+
</div>
229+
<div className="justify-end">
230+
<Button variant="discreet_outline">Send TODO</Button>
231+
</div>
232+
</div>
233+
);
234+
})
235+
)}
236+
{pagesOfContacts && pagesOfContacts?.length > 1 && (
237+
<PaginatedNavigation
238+
className=""
239+
pages={pagesOfContacts}
240+
currentPage={currentPage}
241+
onPageChange={setCurrentPage}
242+
/>
243+
)}
244+
</div>
245+
</div>
246+
);
247+
}

src/views/myData/addProtectedData.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export default function AddProtectedData() {
108108
mutationKey: ['protectData', userAddress],
109109
mutationFn: async (isMail: boolean) => {
110110
const dataProtectorCore = await getDataProtectorCoreClient();
111-
const key = isMail ? 'email' : 'telegramChatId';
111+
const key = isMail ? 'email' : 'telegram_chatId';
112112

113113
const protectedData = await dataProtectorCore.protectData({
114114
data: {

0 commit comments

Comments
 (0)