Files
tlsn-plugin-demo/web/components/Steps/index.tsx
Codetrauma a01d130bdc fix: squash
2024-10-30 10:48:03 -07:00

366 lines
10 KiB
TypeScript

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