Update UI/UX

This commit is contained in:
Shubham Gupta
2024-04-09 20:59:58 +05:30
parent 49b4a81e87
commit 3370c1ee1c
19 changed files with 917 additions and 45 deletions

View File

@@ -1,8 +1,5 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
@@ -32,11 +29,3 @@
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,23 +1,56 @@
import './App.css'
import { ConfigureSafeModule } from './components/ConfigureSafeModule';
import { PerformRecovery } from './components/PerformRecovery';
import { AppContextProvider } from './context/AppContextProvider';
import { createContext, useEffect, useState } from "react";
import "./App.css";
import ConnectWallets from "./components/ConnectWallets";
import Navbar from "./components/Navbar";
import RequestedRecoveries from "./components/RequestedRecoveries";
import RequestGuardian from "./components/RequestGuardian";
import SafeModuleRecovery from "./components/SafeModuleRecovery";
import TriggerAccountRecovery from "./components/TriggerAccountRecovery";
import { STEPS } from "./constants";
import { Web3Provider } from "./providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { useAccount } from "wagmi";
import { AppContextProvider } from "./context/AppContextProvider";
export const StepsContext = createContext(null);
function App() {
const [step, setStep] = useState(STEPS.CONNECT_WALLETS);
const renderBody = () => {
switch (step) {
case STEPS.CONNECT_WALLETS:
return <ConnectWallets />;
case STEPS.SAFE_MODULE_RECOVERY:
return <SafeModuleRecovery />;
case STEPS.REQUEST_GUARDIAN:
return <RequestGuardian />;
case STEPS.REQUESTED_RECOVERIES:
return <RequestedRecoveries />;
case STEPS.TRIGGER_ACCOUNT_RECOVERY:
return <TriggerAccountRecovery />;
default:
return <ConnectWallets />;
}
};
return (
<>
<h1>Safe Email Recovery Demo</h1>
<Web3Provider>
<AppContextProvider>
<ConnectKitButton />
<ConfigureSafeModule />
<PerformRecovery />
</AppContextProvider>
</Web3Provider>
</>
)
<Web3Provider>
<AppContextProvider>
<StepsContext.Provider
value={{
setStep,
}}
>
<div className="app">
<Navbar />
<h1>Safe Email Recovery Demo</h1>
{renderBody()}
</div>
</StepsContext.Provider>{" "}
</AppContextProvider>
</Web3Provider>
);
}
export default App
export default App;

View File

@@ -0,0 +1,3 @@
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2 9.5L19.2005 11.5L17.2 9.5M19.4451 11C19.4814 10.6717 19.5 10.338 19.5 10C19.5 5.02944 15.4706 1 10.5 1C5.52944 1 1.5 5.02944 1.5 10C1.5 14.9706 5.52944 19 10.5 19C13.3273 19 15.85 17.6963 17.5 15.6573M10.5 5V10L13.5 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1,3 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 19H7.5M13.5 19H16.5M16 4.5V12.5M1.5 4.2L1.5 12.8C1.5 13.9201 1.5 14.4802 1.71799 14.908C1.90973 15.2843 2.21569 15.5903 2.59202 15.782C3.01984 16 3.57989 16 4.7 16L16.3 16C17.4201 16 17.9802 16 18.408 15.782C18.7843 15.5903 19.0903 15.2843 19.282 14.908C19.5 14.4802 19.5 13.9201 19.5 12.8V4.2C19.5 3.0799 19.5 2.51984 19.282 2.09202C19.0903 1.7157 18.7843 1.40974 18.408 1.21799C17.9802 1 17.4201 1 16.3 1L4.7 1C3.5799 1 3.01984 1 2.59202 1.21799C2.2157 1.40973 1.90973 1.71569 1.71799 2.09202C1.5 2.51984 1.5 3.07989 1.5 4.2ZM10 8.5C10 9.88071 8.88071 11 7.5 11C6.11929 11 5 9.88071 5 8.5C5 7.11929 6.11929 6 7.5 6C8.88071 6 10 7.11929 10 8.5Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 15V11M11 7H11.01M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="#2E90FA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@@ -0,0 +1,17 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_2084_199)">
<path d="M4.60648 7.25C4.76258 7.54725 5.07428 7.75 5.43333 7.75H6.5C7.05228 7.75 7.5 7.30229 7.5 6.75C7.5 6.19772 7.05228 5.75 6.5 5.75H5.5C4.94772 5.75 4.5 5.30229 4.5 4.75C4.5 4.19772 4.94772 3.75 5.5 3.75H6.56667C6.92572 3.75 7.23742 3.95275 7.39352 4.25M6 3V3.75M6 7.75V8.5M10 6C10 8.45422 7.32302 10.2392 6.349 10.8074C6.2383 10.872 6.18295 10.9043 6.10484 10.9211C6.04422 10.9341 5.95578 10.9341 5.89516 10.9211C5.81705 10.9043 5.7617 10.872 5.65101 10.8074C4.67698 10.2392 2 8.45422 2 6V3.6088C2 3.20904 2 3.00917 2.06538 2.83735C2.12314 2.68557 2.21699 2.55014 2.33883 2.44277C2.47675 2.32122 2.6639 2.25104 3.0382 2.11067L5.7191 1.10534C5.82305 1.06636 5.87502 1.04687 5.92849 1.03914C5.97592 1.03229 6.02408 1.03229 6.07151 1.03914C6.12498 1.04687 6.17695 1.06636 6.2809 1.10534L8.9618 2.11067C9.3361 2.25104 9.52325 2.32122 9.66117 2.44277C9.78301 2.55014 9.87686 2.68557 9.93462 2.83735C10 3.00917 10 3.20904 10 3.6088V6Z" stroke="#F79009" stroke-linecap="round" stroke-linejoin="round" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_2084_199" x="-2.5" y="0.534" width="17" height="18.8968" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2084_199"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2084_199" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 12H14.51M1 3V17C1 18.1046 1.89543 19 3 19H17C18.1046 19 19 18.1046 19 17V7C19 5.89543 18.1046 5 17 5L3 5C1.89543 5 1 4.10457 1 3ZM1 3C1 1.89543 1.89543 1 3 1H15M15 12C15 12.2761 14.7761 12.5 14.5 12.5C14.2239 12.5 14 12.2761 14 12C14 11.7239 14.2239 11.5 14.5 11.5C14.7761 11.5 15 11.7239 15 12Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

View File

@@ -1,11 +1,16 @@
import React from 'react';
import React from "react";
export function Button({ children, ...buttonProps }: React.ComponentPropsWithoutRef<"button">) {
return (
<div className="card">
<button {...buttonProps}>
{children}
</button>
</div>
)
export function Button({
children,
...buttonProps
}: React.ComponentPropsWithoutRef<"button">) {
return (
<div className="button">
<button {...buttonProps}>
{children}
{buttonProps.endIcon ? buttonProps.endIcon : null}
{buttonProps?.loading ? <div className="loader" /> : null}
</button>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { Button } from "./Button";
import walletIcon from "../assets/wallet.svg";
import infoIcon from "../assets/infoIcon.svg";
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { useAccount } from "wagmi";
import { useContext } from "react";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const ConnectWallets = () => {
const { address } = useAccount();
const stepsContext = useContext(StepsContext);
if (address) {
console.log(stepsContext);
stepsContext?.setStep(STEPS.SAFE_MODULE_RECOVERY);
}
return (
<div className="connect-wallets-container">
<Button endIcon={<img src={walletIcon} />}>Connect Genosis Safe</Button>
<p color="#CECFD2" style={{ display: "flex", gap: "0.5rem" }}>
<img src={infoIcon} alt="info" />
Copy the link and import into your safe wallet
</p>
<ConnectKitButton.Custom>
{({ isConnected, show, truncatedAddress, ensName }) => {
return (
<Button onClick={show} endIcon={<img src={walletIcon} />}>
Connect Test Wallet
</Button>
);
}}
</ConnectKitButton.Custom>
<p style={{ textDecoration: "underline" }}>
Or, recover existing wallet instead
</p>
</div>
);
};
export default ConnectWallets;

View File

@@ -0,0 +1,13 @@
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
const Navbar = () => {
return (
<nav className="navbar">
<ConnectKitButton />
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,215 @@
import { useCallback, useContext, useMemo, useState } from "react";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { abi as safeAbi } from "../abi/Safe.json";
import { useAppContext } from "../context/AppContextHook";
import { abi as recoveryPluginAbi } from "../abi/SafeZkEmailRecoveryPlugin.json";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import {
genAccountCode,
getRequestGuardianSubject,
templateIdx,
} from "../utils/email";
import { readContract } from "wagmi/actions";
import { config } from "../providers/config";
import { pad } from "viem";
import { relayer } from "../services/relayer";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const RequestGuardian = () => {
const { address } = useAccount();
const { writeContractAsync } = useWriteContract();
const { guardianEmail, setGuardianEmail, accountCode, setAccountCode } =
useAppContext();
const stepsContext = useContext(StepsContext);
const [recoveryDelay, setRecoveryDelay] = useState(0);
const isMobile = window.innerWidth < 768;
const { data: safeOwnersData } = useReadContract({
address,
abi: safeAbi,
functionName: "getOwners",
});
const firstSafeOwner = useMemo(() => {
const safeOwners = safeOwnersData as string[];
if (!safeOwners?.length) {
return;
}
return safeOwners[0];
}, [safeOwnersData]);
const configureRecoveryAndRequestGuardian = useCallback(async () => {
if (!address) {
throw new Error("unable to get account address");
}
if (!guardianEmail) {
throw new Error("guardian email not set");
}
if (!firstSafeOwner) {
throw new Error("safe owner not found");
}
const acctCode = await genAccountCode();
setAccountCode(accountCode);
const guardianSalt = await relayer.getAccountSalt(acctCode, guardianEmail);
const guardianAddr = await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "computeEmailAuthAddress",
args: [guardianSalt],
});
// TODO Should this be something else?
const previousOwnerInLinkedList = pad("0x1", {
size: 20,
});
try {
await writeContractAsync({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "configureRecovery",
args: [
firstSafeOwner,
guardianAddr,
recoveryDelay,
previousOwnerInLinkedList,
],
});
} catch (error) {
console.log(error);
}
console.debug("recovery configured");
const recoveryRouterAddr = (await readContract(config, {
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "getRouterForSafe",
args: [address],
})) as string;
const subject = getRequestGuardianSubject(address);
const { requestId } = await relayer.acceptanceRequest(
recoveryRouterAddr,
guardianEmail,
acctCode,
templateIdx,
subject
);
let checkGuardianAcceptanceInterval = null
const checkGuardianAcceptance = async () => {
if (!requestId) {
throw new Error("missing guardian request id");
}
const resBody = await relayer.requestStatus(requestId);
console.debug("guardian req res body", resBody);
if(resBody?.is_success) {
stepsContext?.setStep(STEPS.REQUESTED_RECOVERIES);
checkGuardianAcceptanceInterval?.clearInterval()
}
}
checkGuardianAcceptanceInterval = setInterval(async () => {
const res = await checkGuardianAcceptance();
console.log(res)
}, 5000);
// TODO poll until guard req is complete or fails
}, [
address,
firstSafeOwner,
guardianEmail,
recoveryDelay,
accountCode,
setAccountCode,
writeContractAsync,
]);
return (
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<ConnectKitButton />
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Guardian Details:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: "2rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
justifyContent: "space-between",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "60%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div>
<span>Recovery Delay</span>
<input
style={{ width: "1.875rem", marginLeft: "1rem" }}
type="number"
min={0}
value={recoveryDelay}
onChange={(e) => setRecoveryDelay(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div style={{ margin: "auto" }}>
<Button onClick={configureRecoveryAndRequestGuardian}>
Configure Recovery and Request Guardian
</Button>
</div>
</div>
);
};
export default RequestGuardian;

View File

@@ -0,0 +1,266 @@
import { useCallback, useContext, useState } from "react";
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import cancelRecoveryIcon from "../assets/cancelRecoveryIcon.svg";
import completeRecoveryIcon from "../assets/completeRecoveryIcon.svg";
import recoveredIcon from "../assets/recoveredIcon.svg";
import { useAppContext } from "../context/AppContextHook";
import { useAccount, useReadContract } from "wagmi";
import { relayer } from "../services/relayer";
import { abi as recoveryPluginAbi } from "../abi/SafeZkEmailRecoveryPlugin.json";
import { getRequestsRecoverySubject, templateIdx } from "../utils/email";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const BUTTON_STATES = {
TRIGGER_RECOVERY: "Trigger Recovery",
CANCEL_RECOVERY: "Cancel Recovery",
COMPLETE_RECOVERY: "Complete Recovery",
RECOVERY_COMPLETED: "Recovery Completed",
};
const RequestedRecoveries = () => {
const isMobile = window.innerWidth < 768;
const { address } = useAccount();
const { guardianEmail, setGuardianEmail } = useAppContext();
const stepsContext = useContext(StepsContext);
const [newOwner, setNewOwner] = useState<string>();
const [buttonState, setButtonState] = useState(
BUTTON_STATES.TRIGGER_RECOVERY
);
const [loading, setLoading] = useState<boolean>(false);
const [gurdianRequestId, setGuardianRequestId] = useState<number>();
const { data: recoveryRouterAddr } = useReadContract({
abi: recoveryPluginAbi,
address: safeZkSafeZkEmailRecoveryPlugin as `0x${string}`,
functionName: "getRouterForSafe",
args: [address],
});
const requestRecovery = useCallback(async () => {
setLoading(true);
if (!address) {
throw new Error("unable to get account address");
}
if (!guardianEmail) {
throw new Error("guardian email not set");
}
if (!newOwner) {
throw new Error("new owner not set");
}
if (!recoveryRouterAddr) {
throw new Error("could not find recovery router for safe");
}
const subject = getRequestsRecoverySubject(address, newOwner);
const { requestId } = await relayer.recoveryRequest(
recoveryRouterAddr as string,
guardianEmail,
templateIdx,
subject
);
setGuardianRequestId(requestId);
let checkRequestRecoveryStatusInterval = null
const checkGuardianAcceptance = async () => {
if (!requestId) {
throw new Error("missing guardian request id");
}
const resBody = await relayer.requestStatus(requestId);
console.debug("guardian req res body", resBody);
if(resBody?.is_success) {
stepsContext?.setStep(STEPS.REQUESTED_RECOVERIES);
checkRequestRecoveryStatusInterval?.clearInterval()
}
}
checkRequestRecoveryStatusInterval = setInterval(async () => {
const res = await checkGuardianAcceptance();
console.log(res)
}, 5000);
setLoading(false);
setButtonState(BUTTON_STATES.COMPLETE_RECOVERY);
}, [recoveryRouterAddr, address, guardianEmail, newOwner]);
const completeRecovery = useCallback(async () => {
setLoading(true);
if (!recoveryRouterAddr) {
throw new Error("could not find recovery router for safe");
}
const res = relayer.completeRecovery(recoveryRouterAddr as string);
console.debug("complete recovery res", res);
setLoading(false);
setButtonState(BUTTON_STATES.RECOVERY_COMPLETED);
}, [recoveryRouterAddr]);
const checkGuardianAcceptance = useCallback(async () => {
if (!gurdianRequestId) {
throw new Error("missing guardian request id");
}
const resBody = await relayer.requestStatus(gurdianRequestId);
console.debug("guardian req res body", resBody);
}, [gurdianRequestId]);
const getButtonComponent = () => {
switch (buttonState) {
case BUTTON_STATES.TRIGGER_RECOVERY:
return (
<Button
loading={loading}
onClick={requestRecovery}
endIcon={<img src={cancelRecoveryIcon} />}
>
Trigger Recovery
</Button>
);
case BUTTON_STATES.CANCEL_RECOVERY:
return (
<Button endIcon={<img src={cancelRecoveryIcon} />}>
Cancel Recovery
</Button>
);
case BUTTON_STATES.COMPLETE_RECOVERY:
return (
<Button
loading={loading}
onClick={completeRecovery}
endIcon={<img src={completeRecoveryIcon} />}
>
Complete Recovery
</Button>
);
case BUTTON_STATES.RECOVERY_COMPLETED:
return (
<Button
loading={loading}
onClick={() => stepsContext.setStep(STEPS.CONNECT_WALLETS)}
>
Complete! Connect new wallet to set new guardians
</Button>
);
}
};
return (
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
gap: "1rem",
}}
>
<ConnectKitButton />
{buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? (
<div
style={{
background: "#4E1D09",
border: "1px solid #93370D",
color: "#FEC84B",
padding: "0.25rem 0.75rem",
borderRadius: "3.125rem",
width: "fit-content",
height: "fit-content",
}}
>
<img src={recoveredIcon} style={{ marginRight: "0.5rem" }} />
Recovered
</div>
) : null}
</div>
</div>
{buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? null : (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Requested Recoveries:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: isMobile ? "1rem" : "3rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Requested New Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={newOwner}
onChange={(e) => setNewOwner(e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
<div style={{ margin: "auto" }}>{getButtonComponent()}</div>
</div>
);
};
export default RequestedRecoveries;

View File

@@ -0,0 +1,60 @@
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { safeZkSafeZkEmailRecoveryPlugin } from "../../contracts.base-sepolia.json";
import { abi as safeAbi } from "../abi/Safe.json";
import { useCallback, useContext, useState } from "react";
import { StepsContext } from "../App";
import { STEPS } from "../constants";
const SafeModuleRecovery = () => {
const { address } = useAccount();
const { writeContractAsync } = useWriteContract();
const stepsContext = useContext(StepsContext);
const [loading, setLoading] = useState(false);
const { data: isModuleEnabled } = useReadContract({
address,
abi: safeAbi,
functionName: "isModuleEnabled",
args: [safeZkSafeZkEmailRecoveryPlugin],
});
console.log(isModuleEnabled);
if (isModuleEnabled) {
console.log("Module is enabled");
setLoading(false);
stepsContext?.setStep(STEPS.REQUEST_GUARDIAN);
}
const enableEmailRecoveryModule = useCallback(async () => {
setLoading(true);
if (!address) {
throw new Error("unable to get account address");
}
await writeContractAsync({
abi: safeAbi,
address,
functionName: "enableModule",
args: [safeZkSafeZkEmailRecoveryPlugin],
});
}, [address, writeContractAsync]);
return (
<div style={{ display: "flex", gap: "2rem", flexDirection: "column" }}>
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
Connected wallet: <ConnectKitButton />
</div>
{!isModuleEnabled ? (
<Button loading={loading} onClick={enableEmailRecoveryModule}>
Enable email recovery module
</Button>
) : null}
</div>
);
};
export default SafeModuleRecovery;

View File

@@ -0,0 +1,126 @@
import { useState } from "react";
import { Web3Provider } from "../providers/Web3Provider";
import { ConnectKitButton } from "connectkit";
import { Button } from "./Button";
import cancelRecoveryIcon from "../assets/cancelRecoveryIcon.svg";
import completeRecoveryIcon from "../assets/completeRecoveryIcon.svg";
const BUTTON_STATES = {
CANCEL_RECOVERY: "Cancel Recovery",
COMPLETE_RECOVERY: "Complete Recovery",
};
const TriggerAccountRecovery = () => {
const isMobile = window.innerWidth < 768;
const [guardianEmail, setGuardianEmail] = useState("");
const [newWalletAddress, setNewWalletAddress] = useState("");
const [buttonState, setButtonState] = useState(BUTTON_STATES.CANCEL_RECOVERY);
return (
<Web3Provider>
<div
style={{
maxWidth: isMobile ? "100%" : "50%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "flex-start",
gap: "2rem",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
Connected wallet:
<ConnectKitButton />
</div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
width: "100%",
}}
>
Triggered Account Recoveries:
<div className="container">
<div
style={{
display: "flex",
flexDirection: "row",
gap: isMobile ? "1rem" : "3rem",
width: "100%",
alignItems: "flex-end",
flexWrap: "wrap",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Guardian's Email</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>Previous Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={guardianEmail}
onChange={(e) => setGuardianEmail(e.target.value)}
/>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: isMobile ? "90%" : "45%",
}}
>
<p>New Wallet Address</p>
<input
style={{ width: "100%" }}
type="email"
value={newWalletAddress}
onChange={(e) => setNewWalletAddress(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div style={{ margin: "auto" }}>
<Button
endIcon={
buttonState === BUTTON_STATES.CANCEL_RECOVERY ? (
<img src={cancelRecoveryIcon} />
) : (
<img src={completeRecoveryIcon} />
)
}
>
{buttonState === BUTTON_STATES.CANCEL_RECOVERY
? "Cancel "
: "Complete"}
Recovery
</Button>
</div>
</div>
</Web3Provider>
);
};
export default TriggerAccountRecovery;

View File

@@ -0,0 +1,7 @@
export const STEPS = {
CONNECT_WALLETS: 0,
SAFE_MODULE_RECOVERY: 1,
REQUEST_GUARDIAN: 2,
REQUESTED_RECOVERIES: 3,
TRIGGER_ACCOUNT_RECOVERY: 4,
};

View File

@@ -5,7 +5,7 @@
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
background-color: #0C111D;
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -18,21 +18,30 @@ a {
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
}
.app {
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
padding: 0 2rem;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
height: 100vh;
}
h1 {
font-size: 3.2em;
font-size: 2.25rem;
line-height: 1.1;
text-align: center;
font-weight: 600;
}
button {
@@ -40,18 +49,27 @@ button {
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
display: flex;
gap: 1rem;
font-weight: 600;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
border: none;
box-shadow: 0px 1px 2px 0px #1018280D;
background: linear-gradient(354.6deg, #0069E4 37.48%, #37C3FF 107.66%);
border-image-source: linear-gradient(144.35deg, #0069E4 33.65%, #37C3FF 93.17%);
padding: 22px;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
/* outline: 4px auto -webkit-focus-ring-color; */
}
@media (prefers-color-scheme: light) {
@@ -59,10 +77,77 @@ button:focus-visible {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.navbar {
position: absolute;
top: 1.25rem;
right: 1.25rem;
width: 100vw;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.connect-wallets-container {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-direction: column;
width: fit-content;
align-items: center;
}
input {
background: var(--Colors-Background-bg-tertiary, #1F242F);
border: 1px solid var(--Colors-Border-border-primary, #333741);
box-shadow: 0px 1px 2px 0px #1018280D;
border-radius: 4px;
padding: 8px 12px;
color: #85888E;
}
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
background: var(--Colors-Background-bg-secondary, #161B26);
border: 1px solid var(--Colors-Border-border-primary, #333741);
padding: 20px 24px 20px 24px;
gap: 20px;
border-radius: 12px;
border: 1px 0px 0px 0px;
opacity: 0px;
color: #94969C;
font-weight: 500;
}
.loader {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 12px;
height: 12px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}