fix: squash

This commit is contained in:
Codetrauma
2024-10-30 10:48:03 -07:00
parent 1dcf07db5e
commit a01d130bdc
34 changed files with 7084 additions and 1307 deletions

View File

View 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>
);
}

View 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;
}
}
}

View 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>
);
}

View 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;
}
}
}

View 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>
);
}

View File

View 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>
);
}

View File

View 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
View 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
View File

@@ -0,0 +1,4 @@
declare module '*.svg' {
const content: any;
export default content;
}

29
web/index.tsx Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
import * as Comlink from 'comlink';
import init, { Presentation } from 'tlsn-js';
Comlink.expose({
init,
Presentation,
});