mirror of
https://github.com/tlsnotary/tlsn-plugin-demo.git
synced 2026-01-09 21:37:55 -05:00
fix: squash
This commit is contained in:
0
web/components/Body/index.scss
Normal file
0
web/components/Body/index.scss
Normal file
16
web/components/Body/index.tsx
Normal file
16
web/components/Body/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import './index.scss';
|
||||
import Steps from '../Steps';
|
||||
import Logo from '../../../static/logo.svg';
|
||||
|
||||
export default function Body(): ReactElement {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex flex-row w-full justify-center items-center gap-4 pb-12">
|
||||
<img className="w-11 h-11" src={Logo} alt="Logo" />
|
||||
<span className="font-bold text-slate-700 text-3xl">Plugin Demo</span>
|
||||
</div>
|
||||
<Steps />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
web/components/Button/index.scss
Normal file
45
web/components/Button/index.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.button {
|
||||
@apply bg-slate-100;
|
||||
@apply text-slate-500;
|
||||
@apply font-bold;
|
||||
@apply px-2 py-0.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-600;
|
||||
@apply bg-slate-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-700;
|
||||
@apply bg-slate-300;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@apply bg-primary/[0.8];
|
||||
@apply text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-primary/[0.9];
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-50;
|
||||
@apply select-none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
web/components/Button/index.tsx
Normal file
46
web/components/Button/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { ButtonHTMLAttributes, ReactElement } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './index.scss';
|
||||
import Icon from '../Icon';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
btnType?: 'primary' | 'secondary' | '';
|
||||
loading?: boolean;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
export default function Button(props: Props): ReactElement {
|
||||
const {
|
||||
className,
|
||||
btnType = '',
|
||||
children,
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
// Must select all non-button props here otherwise react-dom will show warning
|
||||
...btnProps
|
||||
} = props;
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-row flex-nowrap items-center',
|
||||
'h-10 px-4 button transition-colors',
|
||||
{
|
||||
'button--primary': btnType === 'primary',
|
||||
'button--secondary': btnType === 'secondary',
|
||||
'cursor-default': disabled || loading,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
onClick={!disabled && !loading ? onClick : undefined}
|
||||
disabled={disabled || loading}
|
||||
{...btnProps}
|
||||
>
|
||||
{loading ? (
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={2} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
45
web/components/Header/index.scss
Normal file
45
web/components/Header/index.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.button {
|
||||
@apply bg-slate-100;
|
||||
@apply text-slate-500;
|
||||
@apply font-bold;
|
||||
@apply px-2 py-0.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-600;
|
||||
@apply bg-slate-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-700;
|
||||
@apply bg-slate-300;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@apply bg-primary/[0.8];
|
||||
@apply text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-primary/[0.9];
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-50;
|
||||
@apply select-none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
web/components/Header/index.tsx
Normal file
35
web/components/Header/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import Logo from '../../../static/logo.svg';
|
||||
import './index.scss';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default function Header(): ReactElement {
|
||||
return (
|
||||
<header className="flex flex-row items-center justify-between h-16 px-4 bg-slate-200">
|
||||
<img className="w-8 h-8" src={Logo} />
|
||||
<div className="flex flex-row items-center">
|
||||
<a
|
||||
href="https://tlsnotary.org/"
|
||||
className="flex flex-row items-center justify-center button !bg-transparent"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon fa="fa-solid fa-globe" />
|
||||
</a>
|
||||
<a
|
||||
href=""
|
||||
className="flex flex-row items-center justify-center button !bg-transparent"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon fa="fa-brands fa-github" />
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/9XwESXtcN7"
|
||||
className="flex flex-row items-center justify-center button !bg-transparent"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon fa="fa-brands fa-discord" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
0
web/components/Icon/index.scss
Normal file
0
web/components/Icon/index.scss
Normal file
37
web/components/Icon/index.tsx
Normal file
37
web/components/Icon/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { MouseEventHandler, ReactElement, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import './index.scss';
|
||||
|
||||
type Props = {
|
||||
url?: string;
|
||||
fa?: string;
|
||||
className?: string;
|
||||
size?: number;
|
||||
onClick?: MouseEventHandler;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export default function Icon(props: Props): ReactElement {
|
||||
const { url, size = 1, className = '', fa, onClick, children } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-contain bg-center bg-no-repeat icon',
|
||||
{
|
||||
'cursor-pointer': onClick,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundImage: url ? `url(${url})` : undefined,
|
||||
width: !fa ? `${size}rem` : undefined,
|
||||
height: !fa ? `${size}rem` : undefined,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!url && !!fa && <i className={fa} style={{ fontSize: `${size}rem` }} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
0
web/components/Steps/index.scss
Normal file
0
web/components/Steps/index.scss
Normal file
365
web/components/Steps/index.tsx
Normal file
365
web/components/Steps/index.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import './index.scss';
|
||||
import Stepper from '@mui/material/Stepper';
|
||||
import Step from '@mui/material/Step';
|
||||
import Box from '@mui/material/Box';
|
||||
import StepLabel from '@mui/material/StepLabel';
|
||||
import classNames from 'classnames';
|
||||
import type { PresentationJSON } from 'tlsn-js/build/types';
|
||||
import Button from '../Button';
|
||||
import ConfettiExplosion, { ConfettiProps } from 'react-confetti-explosion';
|
||||
|
||||
const steps = [
|
||||
'Connect Extension',
|
||||
'Install Plugin',
|
||||
'Run Plugin',
|
||||
'🎉 Claim POAP 🎉',
|
||||
];
|
||||
|
||||
export default function Steps(): ReactElement {
|
||||
const [extensionInstalled, setExtensionInstalled] = useState(false);
|
||||
const [pluginID, setPluginID] = useState('');
|
||||
const [step, setStep] = useState<number>(0);
|
||||
const [client, setClient] = useState<any>(null);
|
||||
const [pluginData, setPluginData] = useState<PresentationJSON | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [pluginInstalled, setPluginInstalled] = useState<boolean>(false);
|
||||
const [transcript, setTranscript] = useState<any>(null);
|
||||
const [screenName, setScreenName] = useState<string>('');
|
||||
const [exploding, setExploding] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkExtension = () => {
|
||||
//@ts-ignore
|
||||
if (typeof window.tlsn !== 'undefined') {
|
||||
setExtensionInstalled(true);
|
||||
setTimeout(async () => {
|
||||
// temporary fix until extension events added
|
||||
// @ts-ignore
|
||||
setClient(await window.tlsn.connect());
|
||||
setStep(1);
|
||||
}, 200);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
checkExtension();
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const { default: init } = await import('tlsn-js');
|
||||
await init();
|
||||
})();
|
||||
|
||||
return () => {
|
||||
window.onload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (transcript) {
|
||||
const match = transcript.recv.match(/"screen_name":"([^"]+)"/);
|
||||
const screenName = match ? match[1] : null;
|
||||
setScreenName(screenName);
|
||||
setExploding(true);
|
||||
}
|
||||
}, [transcript]);
|
||||
|
||||
async function handleConnect() {
|
||||
try {
|
||||
//@ts-ignore
|
||||
setClient(await window.tlsn.connect());
|
||||
setStep(1);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetPlugins() {
|
||||
try {
|
||||
const plugins = await client.getPlugins('**', '**', {
|
||||
id: 'twitter-plugin',
|
||||
});
|
||||
if (plugins.length > 0) {
|
||||
setPluginID(plugins[0].hash);
|
||||
setStep(2);
|
||||
} else {
|
||||
setPluginInstalled(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePluginInstall() {
|
||||
try {
|
||||
const plugin = await client.installPlugin(
|
||||
'https://github.com/tlsnotary/tlsn-extension/raw/main/src/assets/plugins/twitter_profile.wasm',
|
||||
{ id: 'twitter-plugin' },
|
||||
);
|
||||
setPluginID(plugin);
|
||||
setStep(2);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunPlugin() {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pluginData = await client.runPlugin(pluginID);
|
||||
setLoading(false);
|
||||
setPluginData(pluginData);
|
||||
setStep(3);
|
||||
} catch (error) {
|
||||
setLoading(false);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
{extensionInstalled ? (
|
||||
<>
|
||||
<div className="flex flex-row items-center gap-2 text-slate-600 font-bold pb-2">
|
||||
Connected{' '}
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-full h-[10px] w-[10px] border-[2px]',
|
||||
{
|
||||
'bg-green-500': step >= 1,
|
||||
'bg-red-500': step === 0,
|
||||
},
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
<Box className="w-full max-w-xl mt-6 pb-4">
|
||||
<Stepper activeStep={step} alternativeLabel>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
</Box>
|
||||
<div className="flex gap-3">
|
||||
{step === 0 && (
|
||||
<button onClick={handleConnect} className="button">
|
||||
Connect
|
||||
</button>
|
||||
)}
|
||||
{step === 1 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<button className="button" onClick={handleGetPlugins}>
|
||||
Check Plugins
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePluginInstall}
|
||||
disabled={!pluginInstalled}
|
||||
className="button"
|
||||
>
|
||||
Install Plugin
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Button onClick={handleRunPlugin} loading={loading}>
|
||||
Run Plugin
|
||||
</Button>
|
||||
<span className="font-bold">
|
||||
Please keep the sidebar open during the notarization process
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<>
|
||||
<ClaimPoap screen_name={screenName} exploding={exploding} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DisplayPluginData
|
||||
step={step}
|
||||
pluginData={pluginData}
|
||||
transcript={transcript}
|
||||
setTranscript={setTranscript}
|
||||
setStep={setStep}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center gap-2">
|
||||
<a
|
||||
href="https://chromewebstore.google.com/detail/tlsn-extension/gcfkkledipjbgdbimfpijgbkhajiaaph"
|
||||
target="_blank"
|
||||
className="button"
|
||||
>
|
||||
Install TLSN Extension
|
||||
</a>
|
||||
<p className="font-bold">Please install the extension to proceed. </p>
|
||||
<p className="font-bold">
|
||||
You will need to refresh your browser after installing the
|
||||
extension.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DisplayPluginData({
|
||||
step,
|
||||
pluginData,
|
||||
transcript,
|
||||
setTranscript,
|
||||
setStep,
|
||||
}: {
|
||||
step: number;
|
||||
pluginData: any;
|
||||
transcript: any;
|
||||
setTranscript: any;
|
||||
setStep: any;
|
||||
}): ReactElement {
|
||||
const [tab, setTab] = useState<'sent' | 'recv'>('sent');
|
||||
|
||||
async function handleVerify() {
|
||||
try {
|
||||
const { Presentation, Transcript } = await import('tlsn-js');
|
||||
const presentation = await new Presentation(pluginData.data);
|
||||
const proof = await presentation.verify();
|
||||
const transcript = new Transcript({
|
||||
sent: proof.transcript.sent,
|
||||
recv: proof.transcript.recv,
|
||||
});
|
||||
const verifiedData = {
|
||||
sent: transcript.sent(),
|
||||
recv: transcript.recv(),
|
||||
};
|
||||
setTranscript(verifiedData);
|
||||
setStep(4);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const formatDataPreview = (data: PresentationJSON) => {
|
||||
if (!data) return '';
|
||||
return Object.entries(data)
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return `${key}: ${JSON.stringify(value, null, 2)}`;
|
||||
} else if (key === 'data') {
|
||||
const maxLength = 160;
|
||||
const previewData = value.toString().substring(0, maxLength);
|
||||
const formattedData = previewData.match(/.{1,20}/g)?.join('\n');
|
||||
return `${key}: ${formattedData}... ${value.length} more`;
|
||||
} else {
|
||||
return `${key}: ${value}`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center space-x-4 mt-8">
|
||||
<div className="w-96">
|
||||
<div className="p-2 bg-gray-200 border-t rounded-t-md text-center text-lg font-semibold">
|
||||
Attestation
|
||||
</div>
|
||||
<div className="p-4 bg-gray-100 border rounded-b-md h-96 text-left overflow-auto">
|
||||
<pre className="text-sm text-gray-700 whitespace-pre-wrap text-[12px]">
|
||||
{formatDataPreview(pluginData)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<button disabled={step !== 3} onClick={handleVerify} className="button">
|
||||
Verify
|
||||
</button>
|
||||
<div className="w-96">
|
||||
<div className="p-2 bg-gray-200 border-t rounded-t-md text-center text-lg font-semibold">
|
||||
Presentation
|
||||
</div>
|
||||
<div className="bg-gray-100 border rounded-b-md h-96 overflow-auto">
|
||||
<div className="flex border-b">
|
||||
<button
|
||||
onClick={() => setTab('sent')}
|
||||
className={`p-2 w-1/2 text-center ${tab === 'sent' ? 'bg-slate-500 text-white' : 'bg-white text-black'}`}
|
||||
>
|
||||
Sent
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTab('recv')}
|
||||
className={`p-2 w-1/2 text-center ${tab === 'recv' ? 'bg-slate-500 text-white' : 'bg-white text-black'}`}
|
||||
>
|
||||
Received
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4 text-left">
|
||||
<pre className="text-[10px] text-gray-700 whitespace-pre-wrap">
|
||||
{transcript &&
|
||||
(tab === 'sent' ? transcript.sent : transcript.recv)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClaimPoap({
|
||||
screen_name,
|
||||
exploding,
|
||||
}: {
|
||||
screen_name: string;
|
||||
exploding: boolean;
|
||||
}): ReactElement {
|
||||
const [screenName, setScreenName] = useState('');
|
||||
const [poapLink, setPoapLink] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClaimPoap = async () => {
|
||||
try {
|
||||
if (!screen_name) return;
|
||||
const response = await fetch('/poap-claim', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ screenName: screen_name }),
|
||||
});
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
setPoapLink(data.poapLink);
|
||||
} else {
|
||||
setError(await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
handleClaimPoap();
|
||||
}, [screen_name]);
|
||||
|
||||
const mediumProps: ConfettiProps = {
|
||||
force: 0.6,
|
||||
duration: 4000,
|
||||
particleCount: 150,
|
||||
width: 1500,
|
||||
colors: ['#F0FFF', '#F0F8FF', '#483D8B', '#E0FFF', '#778899'],
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{poapLink !== '' && (
|
||||
<a className="button" href={poapLink} target="_blank">
|
||||
Claim POAP!
|
||||
</a>
|
||||
)}
|
||||
{exploding && <ConfettiExplosion {...mediumProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
web/components/index.scss
Normal file
45
web/components/index.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.button {
|
||||
@apply bg-slate-100;
|
||||
@apply text-slate-500;
|
||||
@apply font-bold;
|
||||
@apply px-2 py-0.5;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-600;
|
||||
@apply bg-slate-200;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-700;
|
||||
@apply bg-slate-300;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
@apply bg-primary/[0.8];
|
||||
@apply text-white;
|
||||
|
||||
&:hover {
|
||||
@apply bg-primary/[0.9];
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply bg-primary;
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-50;
|
||||
@apply select-none;
|
||||
|
||||
&:hover {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
&:active {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
web/declarations.d.ts
vendored
Normal file
4
web/declarations.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
29
web/index.tsx
Normal file
29
web/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'isomorphic-fetch';
|
||||
import type {} from 'redux-thunk/extend-redux';
|
||||
import * as React from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './pages/App';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureAppStore from './store';
|
||||
|
||||
// @ts-ignore
|
||||
const store = configureAppStore(window.__PRELOADED_STATE__);
|
||||
|
||||
// @ts-ignore
|
||||
delete window.__PRELOADED_STATE__;
|
||||
|
||||
(async () => {
|
||||
hydrateRoot(
|
||||
document.getElementById('root')!,
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
);
|
||||
})();
|
||||
|
||||
if ((module as any).hot) {
|
||||
(module as any).hot.accept();
|
||||
}
|
||||
10
web/pages/App/index.scss
Normal file
10
web/pages/App/index.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
|
||||
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/brands";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/solid";
|
||||
@import "../../../node_modules/@fortawesome/fontawesome-free/scss/regular";
|
||||
16
web/pages/App/index.tsx
Normal file
16
web/pages/App/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import './index.scss';
|
||||
import Header from '../../components/Header';
|
||||
import Body from '../../components/Body';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
|
||||
export default function App(): ReactElement {
|
||||
return (
|
||||
<div className="app flex flex-col gap-4">
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Body />} />
|
||||
</Routes>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
51
web/store/attestation.tsx
Normal file
51
web/store/attestation.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Attestation, AttestedData } from '../utils/types';
|
||||
|
||||
enum ActionType {
|
||||
SET_ATTESTATION = 'attestation/SET_ATTESTATION',
|
||||
}
|
||||
|
||||
export type Action<payload = any> = {
|
||||
type: ActionType;
|
||||
payload: payload;
|
||||
error?: boolean;
|
||||
meta?: any;
|
||||
};
|
||||
|
||||
type AttestationData = {
|
||||
raw: Attestation;
|
||||
};
|
||||
|
||||
export type State = {
|
||||
raw: Attestation;
|
||||
};
|
||||
|
||||
export const initState: State = {
|
||||
raw: {
|
||||
version: '0.1.0-alpha.7',
|
||||
data: '',
|
||||
meta: {
|
||||
notaryUrl: '',
|
||||
websocketProxyUrl: '',
|
||||
pluginUrl: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const setAttestation = (
|
||||
attestation: AttestationData,
|
||||
): Action<AttestationData> => ({
|
||||
type: ActionType.SET_ATTESTATION,
|
||||
payload: attestation,
|
||||
});
|
||||
|
||||
export default function attestation(state = initState, action: Action): State {
|
||||
switch (action.type) {
|
||||
case ActionType.SET_ATTESTATION:
|
||||
return {
|
||||
...state,
|
||||
raw: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
29
web/store/index.tsx
Normal file
29
web/store/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import attestation from './attestation';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
attestation,
|
||||
});
|
||||
|
||||
export type AppRootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
const createStoreWithMiddleware =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? applyMiddleware(
|
||||
thunk,
|
||||
createLogger({
|
||||
collapsed: true,
|
||||
}),
|
||||
)(createStore)
|
||||
: applyMiddleware(thunk)(createStore);
|
||||
|
||||
function configureAppStore(preloadedState?: AppRootState) {
|
||||
const { attestation } = preloadedState || {};
|
||||
return createStoreWithMiddleware(rootReducer, {
|
||||
attestation,
|
||||
});
|
||||
}
|
||||
|
||||
export default configureAppStore;
|
||||
20
web/utils/types.tsx
Normal file
20
web/utils/types.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface AttestedData {
|
||||
version: '0.1.0-alpha.7' | '0.1.0-alpha.5';
|
||||
time: number;
|
||||
sent: string;
|
||||
recv: string;
|
||||
notaryUrl: string;
|
||||
notaryKey: string;
|
||||
websocketProxyUrl?: string;
|
||||
verifierKey?: string;
|
||||
}
|
||||
|
||||
export type Attestation = {
|
||||
version: '0.1.0-alpha.7';
|
||||
data: string;
|
||||
meta: {
|
||||
notaryUrl: string;
|
||||
websocketProxyUrl: string;
|
||||
pluginUrl?: string;
|
||||
};
|
||||
};
|
||||
7
web/utils/worker.ts
Normal file
7
web/utils/worker.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import * as Comlink from 'comlink';
|
||||
import init, { Presentation } from 'tlsn-js';
|
||||
|
||||
Comlink.expose({
|
||||
init,
|
||||
Presentation,
|
||||
});
|
||||
Reference in New Issue
Block a user