From 3370c1ee1c6330674492b5a78309e9e5b4cc891e Mon Sep 17 00:00:00 2001 From: Shubham Gupta Date: Tue, 9 Apr 2024 20:59:58 +0530 Subject: [PATCH] Update UI/UX --- packages/demos/email-recovery/src/App.css | 13 +- packages/demos/email-recovery/src/App.tsx | 65 +++-- .../src/assets/cancelRecoveryIcon.svg | 3 + .../src/assets/cancelRecoveryIcon.zip | Bin 0 -> 383 bytes .../src/assets/completeRecoveryIcon.svg | 3 + .../src/assets/completeRecoveryIcon.zip | Bin 0 -> 551 bytes .../email-recovery/src/assets/infoIcon.svg | 3 + .../src/assets/recoveredIcon.svg | 17 ++ .../email-recovery/src/assets/wallet.svg | 3 + .../email-recovery/src/assets/wallet.zip | Bin 0 -> 404 bytes .../email-recovery/src/components/Button.tsx | 23 +- .../src/components/ConnectWallets.tsx | 44 +++ .../email-recovery/src/components/Navbar.tsx | 13 + .../src/components/RequestGuardian.tsx | 215 ++++++++++++++ .../src/components/RequestedRecoveries.tsx | 266 ++++++++++++++++++ .../src/components/SafeModuleRecovery.tsx | 60 ++++ .../src/components/TriggerAccountRecovery.tsx | 126 +++++++++ .../demos/email-recovery/src/constants.ts | 7 + packages/demos/email-recovery/src/index.css | 101 ++++++- 19 files changed, 917 insertions(+), 45 deletions(-) create mode 100644 packages/demos/email-recovery/src/assets/cancelRecoveryIcon.svg create mode 100644 packages/demos/email-recovery/src/assets/cancelRecoveryIcon.zip create mode 100644 packages/demos/email-recovery/src/assets/completeRecoveryIcon.svg create mode 100644 packages/demos/email-recovery/src/assets/completeRecoveryIcon.zip create mode 100644 packages/demos/email-recovery/src/assets/infoIcon.svg create mode 100644 packages/demos/email-recovery/src/assets/recoveredIcon.svg create mode 100644 packages/demos/email-recovery/src/assets/wallet.svg create mode 100644 packages/demos/email-recovery/src/assets/wallet.zip create mode 100644 packages/demos/email-recovery/src/components/ConnectWallets.tsx create mode 100644 packages/demos/email-recovery/src/components/Navbar.tsx create mode 100644 packages/demos/email-recovery/src/components/RequestGuardian.tsx create mode 100644 packages/demos/email-recovery/src/components/RequestedRecoveries.tsx create mode 100644 packages/demos/email-recovery/src/components/SafeModuleRecovery.tsx create mode 100644 packages/demos/email-recovery/src/components/TriggerAccountRecovery.tsx create mode 100644 packages/demos/email-recovery/src/constants.ts diff --git a/packages/demos/email-recovery/src/App.css b/packages/demos/email-recovery/src/App.css index b9d355df..617ee8d5 100644 --- a/packages/demos/email-recovery/src/App.css +++ b/packages/demos/email-recovery/src/App.css @@ -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; -} diff --git a/packages/demos/email-recovery/src/App.tsx b/packages/demos/email-recovery/src/App.tsx index c435090f..a03289ea 100644 --- a/packages/demos/email-recovery/src/App.tsx +++ b/packages/demos/email-recovery/src/App.tsx @@ -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 ; + case STEPS.SAFE_MODULE_RECOVERY: + return ; + case STEPS.REQUEST_GUARDIAN: + return ; + case STEPS.REQUESTED_RECOVERIES: + return ; + case STEPS.TRIGGER_ACCOUNT_RECOVERY: + return ; + default: + return ; + } + }; + return ( - <> -

Safe Email Recovery Demo

- - - - - - - - - ) + + + +
+ +

Safe Email Recovery Demo

+ {renderBody()} +
+
{" "} +
+
+ ); } -export default App +export default App; diff --git a/packages/demos/email-recovery/src/assets/cancelRecoveryIcon.svg b/packages/demos/email-recovery/src/assets/cancelRecoveryIcon.svg new file mode 100644 index 00000000..435c03c1 --- /dev/null +++ b/packages/demos/email-recovery/src/assets/cancelRecoveryIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/demos/email-recovery/src/assets/cancelRecoveryIcon.zip b/packages/demos/email-recovery/src/assets/cancelRecoveryIcon.zip new file mode 100644 index 0000000000000000000000000000000000000000..c9465de6af0044d5f572fea109613deb6c50939e GIT binary patch literal 383 zcmWIWW@h1HU|`^2P-g6kFn0cT`7e;Sn2~`&07yHPmXze@73+H@=jZ7am!*eJuq|#j z;A#8*himS4uKI+Y>cX~7ZCk6?U3f6%mduXAy1HDUqPHr0`{gn()a{(7cuS9`t-vwi z#oJ1bmCISyZR^kfm&U=$72}XLzf6X=@ZMLJy!(rzc_f`?#C0nj>j+#h|2*sRDPk`T zEZ=;)tjTjh@={TojL9kG%QILwd!8zp5!X@`iMx_E}@;LdbC!$ z?7sDB?#1L!!p~Ty8T?*Yf4>#(x%O3l`N;`4|L=Gwzpvu8S=^HQPvzyx l_6K+~GKnzY4lH1JAp!KD3-D%T14%Oip&gLc1_m($0{|8zmO}sl literal 0 HcmV?d00001 diff --git a/packages/demos/email-recovery/src/assets/completeRecoveryIcon.svg b/packages/demos/email-recovery/src/assets/completeRecoveryIcon.svg new file mode 100644 index 00000000..09845d56 --- /dev/null +++ b/packages/demos/email-recovery/src/assets/completeRecoveryIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/demos/email-recovery/src/assets/completeRecoveryIcon.zip b/packages/demos/email-recovery/src/assets/completeRecoveryIcon.zip new file mode 100644 index 0000000000000000000000000000000000000000..edc57e48a53a1220584ae12ca5c98e55d02a50b1 GIT binary patch literal 551 zcmWIWW@h1HU|`^2&}Qt4xbaQ!+)_pc247|d1_7X`Q)x*_eqOP@XL5d?UU6A^Xs~th zV+DbCpS8e~H4$DiCzD?<`twIeK`?$Rf7owZ2mb5lm30m$ zzKdQM=y0F+c-^xY&L1+no__ywm5Y_j#OcjmzU9H%$1>lr)YQ&7maYA(((Zov)lE7@ zE1#BFhDB+HeoXpU@x7pKX0^qZr2HkUOYHq_*B&dP$G z!EbIDrClzcmj3CX*_9>hXKh&fkUKb4W3eswj&svr?#(`WiNARJ)cxD`FEH^teB*De z<@QHLf2H~VZ@s@P@y;*N{hxl^l7GLCF~FOVNrVA+qypm=37|)DfHx}}NSYA{?SS-U JU~*t!0008B=X(GE literal 0 HcmV?d00001 diff --git a/packages/demos/email-recovery/src/assets/infoIcon.svg b/packages/demos/email-recovery/src/assets/infoIcon.svg new file mode 100644 index 00000000..55284b5b --- /dev/null +++ b/packages/demos/email-recovery/src/assets/infoIcon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/demos/email-recovery/src/assets/recoveredIcon.svg b/packages/demos/email-recovery/src/assets/recoveredIcon.svg new file mode 100644 index 00000000..9d213885 --- /dev/null +++ b/packages/demos/email-recovery/src/assets/recoveredIcon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/demos/email-recovery/src/assets/wallet.svg b/packages/demos/email-recovery/src/assets/wallet.svg new file mode 100644 index 00000000..30a56249 --- /dev/null +++ b/packages/demos/email-recovery/src/assets/wallet.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/demos/email-recovery/src/assets/wallet.zip b/packages/demos/email-recovery/src/assets/wallet.zip new file mode 100644 index 0000000000000000000000000000000000000000..8464606e3a52770c8bc3a6f1c731592b10b11934 GIT binary patch literal 404 zcmWIWW@h1HU|`^2=-AX5QT=$gu@EBz!v`Q10E#-5mXze@73+H@=jZ7am!*dWZp=HZ zAaM7ysQR}gKA(=&6BfqVT*+?B34DBO%esv@QlEaZ?q0I8-KGXaQ0~F ztOB>-ZrwNQ*!~-{)p#U6RB-H>z3+O;25097->&j+R-0S=xOiIS`<3EX{Bn;; zCPx~mwV$oC+0Can>&lE^&J(L=Y;+TF4^d@$r?IJq+oQbo3~K_TVaUdrQAdm9@?J^o zVm-0sXD(|!>!;8LoyoBc3jMJwomBk_3+Bw4qIrCw=qahk(x+8})@^H?8N4+`ZvOs% zjcott6zO%b*k2H@?eMtjY?Z+qiZ9zqt6{C82Ec-yD;Va|?ZU*)4Uw+jx)X z?Cr`cDVy#eek%Vb@$7^TIyy>vPd5By4De=T5@Em{g1|sT0_fox;LXYgl4b-#J0R@< I40i?w0GeQ>TmS$7 literal 0 HcmV?d00001 diff --git a/packages/demos/email-recovery/src/components/Button.tsx b/packages/demos/email-recovery/src/components/Button.tsx index 67e9d9cd..6e89d188 100644 --- a/packages/demos/email-recovery/src/components/Button.tsx +++ b/packages/demos/email-recovery/src/components/Button.tsx @@ -1,11 +1,16 @@ -import React from 'react'; +import React from "react"; -export function Button({ children, ...buttonProps }: React.ComponentPropsWithoutRef<"button">) { - return ( -
- -
- ) +export function Button({ + children, + ...buttonProps +}: React.ComponentPropsWithoutRef<"button">) { + return ( +
+ +
+ ); } diff --git a/packages/demos/email-recovery/src/components/ConnectWallets.tsx b/packages/demos/email-recovery/src/components/ConnectWallets.tsx new file mode 100644 index 00000000..d65481ab --- /dev/null +++ b/packages/demos/email-recovery/src/components/ConnectWallets.tsx @@ -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 ( +
+ + +

+ info + Copy the link and import into your safe wallet +

+ + {({ isConnected, show, truncatedAddress, ensName }) => { + return ( + + ); + }} + +

+ Or, recover existing wallet instead ➔ +

+
+ ); +}; + +export default ConnectWallets; diff --git a/packages/demos/email-recovery/src/components/Navbar.tsx b/packages/demos/email-recovery/src/components/Navbar.tsx new file mode 100644 index 00000000..cede0e57 --- /dev/null +++ b/packages/demos/email-recovery/src/components/Navbar.tsx @@ -0,0 +1,13 @@ +import { Web3Provider } from "../providers/Web3Provider"; +import { ConnectKitButton } from "connectkit"; +import { Button } from "./Button"; + +const Navbar = () => { + return ( + + ); +}; + +export default Navbar; diff --git a/packages/demos/email-recovery/src/components/RequestGuardian.tsx b/packages/demos/email-recovery/src/components/RequestGuardian.tsx new file mode 100644 index 00000000..10c11d4a --- /dev/null +++ b/packages/demos/email-recovery/src/components/RequestGuardian.tsx @@ -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 ( +
+
+ Connected wallet: + +
+
+ Guardian Details: +
+
+
+

Guardian's Email

+ setGuardianEmail(e.target.value)} + /> +
+
+ Recovery Delay + setRecoveryDelay(e.target.value)} + /> +
+
+
+
+
+ +
+
+ ); +}; + +export default RequestGuardian; diff --git a/packages/demos/email-recovery/src/components/RequestedRecoveries.tsx b/packages/demos/email-recovery/src/components/RequestedRecoveries.tsx new file mode 100644 index 00000000..42957c96 --- /dev/null +++ b/packages/demos/email-recovery/src/components/RequestedRecoveries.tsx @@ -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(); + const [buttonState, setButtonState] = useState( + BUTTON_STATES.TRIGGER_RECOVERY + ); + const [loading, setLoading] = useState(false); + const [gurdianRequestId, setGuardianRequestId] = useState(); + + 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 ( + + ); + case BUTTON_STATES.CANCEL_RECOVERY: + return ( + + ); + case BUTTON_STATES.COMPLETE_RECOVERY: + return ( + + ); + case BUTTON_STATES.RECOVERY_COMPLETED: + return ( + + ); + } + }; + + return ( +
+
+ Connected wallet: +
+ + {buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? ( +
+ + Recovered +
+ ) : null} +
+
+ {buttonState === BUTTON_STATES.RECOVERY_COMPLETED ? null : ( +
+ Requested Recoveries: +
+
+
+

Guardian's Email

+ setGuardianEmail(e.target.value)} + /> +
+
+

Requested New Wallet Address

+ setNewOwner(e.target.value)} + /> +
+
+
+
+ )} +
{getButtonComponent()}
+
+ ); +}; + +export default RequestedRecoveries; diff --git a/packages/demos/email-recovery/src/components/SafeModuleRecovery.tsx b/packages/demos/email-recovery/src/components/SafeModuleRecovery.tsx new file mode 100644 index 00000000..9c0a63ca --- /dev/null +++ b/packages/demos/email-recovery/src/components/SafeModuleRecovery.tsx @@ -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 ( +
+
+ Connected wallet: +
+ {!isModuleEnabled ? ( + + ) : null} +
+ ); +}; + +export default SafeModuleRecovery; diff --git a/packages/demos/email-recovery/src/components/TriggerAccountRecovery.tsx b/packages/demos/email-recovery/src/components/TriggerAccountRecovery.tsx new file mode 100644 index 00000000..e054760c --- /dev/null +++ b/packages/demos/email-recovery/src/components/TriggerAccountRecovery.tsx @@ -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 ( + +
+
+ Connected wallet: + +
+
+ Triggered Account Recoveries: +
+
+
+

Guardian's Email

+ setGuardianEmail(e.target.value)} + /> +
+
+

Previous Wallet Address

+ setGuardianEmail(e.target.value)} + /> +
+
+

New Wallet Address

+ setNewWalletAddress(e.target.value)} + /> +
+
+
+
+
+ +
+
+
+ ); +}; + +export default TriggerAccountRecovery; diff --git a/packages/demos/email-recovery/src/constants.ts b/packages/demos/email-recovery/src/constants.ts new file mode 100644 index 00000000..e458dd63 --- /dev/null +++ b/packages/demos/email-recovery/src/constants.ts @@ -0,0 +1,7 @@ +export const STEPS = { + CONNECT_WALLETS: 0, + SAFE_MODULE_RECOVERY: 1, + REQUEST_GUARDIAN: 2, + REQUESTED_RECOVERIES: 3, + TRIGGER_ACCOUNT_RECOVERY: 4, +}; \ No newline at end of file diff --git a/packages/demos/email-recovery/src/index.css b/packages/demos/email-recovery/src/index.css index 6119ad9a..31239db1 100644 --- a/packages/demos/email-recovery/src/index.css +++ b/packages/demos/email-recovery/src/index.css @@ -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); + } +} \ No newline at end of file