From 1939751e7fe7ff687c8f9fa0fc760fa96586ea3e Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 1 Nov 2022 12:02:27 +0100 Subject: [PATCH 1/8] feat: Implement create safe status redesign --- src/components/new-safe/CardStepper/index.tsx | 26 +- .../new-safe/CardStepper/styles.module.css | 2 +- src/components/new-safe/CreateSafe/index.tsx | 24 +- .../new-safe/CreateSafe/useCreateSafe.ts | 27 ++ src/components/new-safe/steps/Step1/index.tsx | 8 +- src/components/new-safe/steps/Step2/index.tsx | 9 +- src/components/new-safe/steps/Step3/index.tsx | 178 ++++++++------ .../new-safe/steps/Step4/StatusMessage.tsx | 92 +++++++ .../new-safe/steps/Step4/StatusStep.tsx | 45 ++++ .../new-safe/steps/Step4/StatusStepper.tsx | 70 ++++++ src/components/new-safe/steps/Step4/index.tsx | 128 ++++++++++ .../new-safe/steps/Step4/logic/index.ts | 232 ++++++++++++++++++ .../new-safe/steps/Step4/styles.module.css | 18 ++ .../new-safe/steps/Step4/useSafeCreation.ts | 100 ++++++++ .../steps/Step4/useSafeCreationEffects.ts | 68 +++++ 15 files changed, 923 insertions(+), 104 deletions(-) create mode 100644 src/components/new-safe/CreateSafe/useCreateSafe.ts create mode 100644 src/components/new-safe/steps/Step4/StatusMessage.tsx create mode 100644 src/components/new-safe/steps/Step4/StatusStep.tsx create mode 100644 src/components/new-safe/steps/Step4/StatusStepper.tsx create mode 100644 src/components/new-safe/steps/Step4/index.tsx create mode 100644 src/components/new-safe/steps/Step4/logic/index.ts create mode 100644 src/components/new-safe/steps/Step4/styles.module.css create mode 100644 src/components/new-safe/steps/Step4/useSafeCreation.ts create mode 100644 src/components/new-safe/steps/Step4/useSafeCreationEffects.ts diff --git a/src/components/new-safe/CardStepper/index.tsx b/src/components/new-safe/CardStepper/index.tsx index e9633c0f0e..1dd9bbe221 100644 --- a/src/components/new-safe/CardStepper/index.tsx +++ b/src/components/new-safe/CardStepper/index.tsx @@ -12,18 +12,20 @@ export function CardStepper(props: TxStepperProps) { return ( - - {activeStep + 1} - - } - className={css.header} - /> + {currentStep.title && ( + + {activeStep + 1} + + } + className={css.header} + /> + )} {currentStep.render(stepData, onSubmit, onBack, setStep)} ) diff --git a/src/components/new-safe/CardStepper/styles.module.css b/src/components/new-safe/CardStepper/styles.module.css index 8ea2864b08..db524373b1 100644 --- a/src/components/new-safe/CardStepper/styles.module.css +++ b/src/components/new-safe/CardStepper/styles.module.css @@ -4,6 +4,7 @@ .header { padding: var(--space-3) var(--space-2); + border-bottom: 1px solid var(--color-border-light); } .header :global .MuiCardHeader-title { @@ -22,7 +23,6 @@ .content { padding: var(--space-3) 52px; - border-top: 1px solid var(--color-border-light); border-bottom: 1px solid var(--color-border-light); } diff --git a/src/components/new-safe/CreateSafe/index.tsx b/src/components/new-safe/CreateSafe/index.tsx index 13b072a23f..ea01e1e8ed 100644 --- a/src/components/new-safe/CreateSafe/index.tsx +++ b/src/components/new-safe/CreateSafe/index.tsx @@ -1,3 +1,4 @@ +import React from 'react' import WalletInfo from '@/components/common/WalletInfo' import { useCurrentChain } from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' @@ -14,30 +15,46 @@ import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' import { CREATE_SAFE_CATEGORY } from '@/services/analytics' import CreateSafeStep3 from '@/components/new-safe/steps/Step3' +import { CreateSafeStatus } from '@/components/new-safe/steps/Step4' export type NewSafeFormData = { name: string threshold: number owners: NamedAddress[] mobileOwners: NamedAddress[] + saltNonce: number + safeAddress?: string } export const CreateSafeSteps: TxStepperProps['steps'] = [ { title: 'Select network and name Safe', subtitle: 'Select the network on which to create your Safe', - render: (data, onSubmit, onBack) => , + render: (data, onSubmit, onBack, setStep) => ( + + ), }, { title: 'Owners and confirmations', subtitle: 'Here you can add owners to your Safe and determine how many owners need to confirm before making a successful transaction', - render: (data, onSubmit, onBack) => , + render: (data, onSubmit, onBack, setStep) => ( + + ), }, { title: 'Review', subtitle: `You're about to create a new Safe and will have to confirm a transaction with your currently connected wallet.`, - render: (data, onSubmit, onBack) => , + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: '', + subtitle: '', + render: (data, onSubmit, onBack, setStep) => ( + + ), }, ] @@ -56,6 +73,7 @@ const CreateSafe = () => { mobileOwners: [] as NamedAddress[], owners: [defaultOwner], threshold: 1, + saltNonce: 0, } const onClose = () => { diff --git a/src/components/new-safe/CreateSafe/useCreateSafe.ts b/src/components/new-safe/CreateSafe/useCreateSafe.ts new file mode 100644 index 0000000000..3d3a52f7e8 --- /dev/null +++ b/src/components/new-safe/CreateSafe/useCreateSafe.ts @@ -0,0 +1,27 @@ +import useIsWrongChain from '@/hooks/useIsWrongChain' +import useWallet from '@/hooks/wallets/useWallet' +import { useEffect } from 'react' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' +import type { PendingSafeData } from '@/components/new-safe/steps/Step4' +import type { NewSafeFormData } from '@/components/new-safe/CreateSafe/index' +import { SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/steps/Step4' + +const useCreateSafe = (setStep: StepRenderProps['setStep']) => { + const [pendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + const wallet = useWallet() + const isWrongChain = useIsWrongChain() + + useEffect(() => { + if (isWrongChain || !wallet) { + setStep(0) + } + + // Jump to the status screen if there is already a tx submitted + if (pendingSafe) { + setStep(3) + } + }, [setStep, isWrongChain, wallet, pendingSafe]) +} + +export default useCreateSafe diff --git a/src/components/new-safe/steps/Step1/index.tsx b/src/components/new-safe/steps/Step1/index.tsx index 064f93e4d6..39369262b4 100644 --- a/src/components/new-safe/steps/Step1/index.tsx +++ b/src/components/new-safe/steps/Step1/index.tsx @@ -19,6 +19,7 @@ import css from './styles.module.css' import NetworkSelector from '@/components/common/NetworkSelector' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' +import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' type CreateSafeStep1Form = { name: string @@ -30,11 +31,8 @@ enum CreateSafeStep1Fields { const STEP_1_FORM_ID = 'create-safe-step-1-form' -function CreateSafeStep1({ - data, - onSubmit, - onBack, -}: Pick, 'onSubmit' | 'data' | 'onBack'>) { +function CreateSafeStep1({ data, onSubmit, onBack, setStep }: StepRenderProps) { + useCreateSafe(setStep) const fallbackName = useMnemonicSafeName() const { diff --git a/src/components/new-safe/steps/Step2/index.tsx b/src/components/new-safe/steps/Step2/index.tsx index 503788f8e6..6a8e43aa6a 100644 --- a/src/components/new-safe/steps/Step2/index.tsx +++ b/src/components/new-safe/steps/Step2/index.tsx @@ -1,6 +1,5 @@ import { Button, Grid, SvgIcon, MenuItem, Select, Tooltip, Typography, Divider, Box } from '@mui/material' import { FormProvider, useFieldArray, useForm } from 'react-hook-form' -import type { ReactElement } from 'react' import AddIcon from '@/public/images/common/add.svg' import InfoIcon from '@/public/images/notifications/info.svg' @@ -8,6 +7,7 @@ import { OwnerRow } from './OwnerRow' import type { NamedAddress } from '@/components/create-safe/types' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' +import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' type CreateSafeStep2Form = { owners: NamedAddress[] @@ -23,11 +23,8 @@ enum CreateSafeStep2Fields { const STEP_2_FORM_ID = 'create-safe-step-2-form' -const CreateSafeStep2 = ({ - onSubmit, - onBack, - data, -}: Pick, 'onSubmit' | 'data' | 'onBack'>): ReactElement => { +const CreateSafeStep2 = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { + useCreateSafe(setStep) const formMethods = useForm({ mode: 'all', defaultValues: { diff --git a/src/components/new-safe/steps/Step3/index.tsx b/src/components/new-safe/steps/Step3/index.tsx index d5ca2f8909..ce62435f50 100644 --- a/src/components/new-safe/steps/Step3/index.tsx +++ b/src/components/new-safe/steps/Step3/index.tsx @@ -1,5 +1,5 @@ import { useMemo, type ReactElement } from 'react' -import { FormProvider, useForm } from 'react-hook-form' +import { useForm } from 'react-hook-form' import { Button, Grid, Typography, Divider, Box } from '@mui/material' import ChainIndicator from '@/components/common/ChainIndicator' import EthHashInfo from '@/components/common/EthHashInfo' @@ -11,6 +11,13 @@ import { formatVisualAmount } from '@/utils/formatters' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' import css from './styles.module.css' +import { getFallbackHandlerContractInstance } from '@/services/contracts/safeContracts' +import { computeNewSafeAddress } from '@/components/create-safe/logic' +import useWallet from '@/hooks/wallets/useWallet' +import { useWeb3 } from '@/hooks/wallets/web3' +import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { type PendingSafeData, SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/steps/Step4' enum CreateSafeStep3Fields { name = 'name', @@ -40,14 +47,14 @@ const ReviewRow = ({ name, value }: { name: string; value: ReactElement }) => { ) } -const CreateSafeStep3 = ({ - onSubmit, - onBack, - data, -}: Pick, 'onSubmit' | 'data' | 'onBack'>): ReactElement => { +const CreateSafeStep3 = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { + useCreateSafe(setStep) const chain = useCurrentChain() + const wallet = useWallet() + const provider = useWeb3() const { maxFeePerGas, maxPriorityFeePerGas } = useGasPrice() const saltNonce = useMemo(() => Date.now(), []) + const [_, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) const safeParams = useMemo(() => { return { @@ -73,89 +80,106 @@ const CreateSafeStep3 = ({ }, }) - const { handleSubmit, getValues } = formMethods - - const allFormData = getValues() + const { getValues } = formMethods const handleBack = () => { + const allFormData = getValues() onBack(allFormData) } + const createSafe = async () => { + if (!wallet || !provider || !chain) return + + const fallbackHandler = getFallbackHandlerContractInstance(chain.chainId) + + const props = { + safeAccountConfig: { + threshold: data.threshold, + owners: data.owners.map((owner) => owner.address), + fallbackHandler: fallbackHandler.address, + }, + safeDeploymentConfig: { + saltNonce: saltNonce.toString(), + }, + } + + const safeAddress = await computeNewSafeAddress(provider, props) + + setPendingSafe({ ...data, saltNonce, safeAddress }) + onSubmit({ ...data, saltNonce, safeAddress }) + } + return ( -
- + + - - - } /> - {data.name}} /> - - {data.owners.map((owner, index) => ( - - ))} - - } - /> - - {data.threshold} out of {data.owners.length} owner(s) - - } - /> - - + } /> + {data.name}} /> + + {data.owners.map((owner, index) => ( + + ))} + + } + /> + + {data.threshold} out of {data.owners.length} owner(s) + + } + /> + + - - - - - - - - ≈ {totalFee} {chain?.nativeCurrency.symbol} - - - - } - /> - - - - You will have to confirm a transaction with your currently connected wallet. + + + + + + + + ≈ {totalFee} {chain?.nativeCurrency.symbol} + - - + + } + /> + + + + You will have to confirm a transaction with your currently connected wallet. + - - - - - - - - -
+ + + + + + + + + ) } diff --git a/src/components/new-safe/steps/Step4/StatusMessage.tsx b/src/components/new-safe/steps/Step4/StatusMessage.tsx new file mode 100644 index 0000000000..c362924a6c --- /dev/null +++ b/src/components/new-safe/steps/Step4/StatusMessage.tsx @@ -0,0 +1,92 @@ +import { Box, Typography } from '@mui/material' +import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import css from '@/components/create-safe/status/styles.module.css' +import classNames from 'classnames' + +const getStep = (status: SafeCreationStatus) => { + const loading = Safe logo + const indexed = Safe logo + const error = Safe logo + + switch (status) { + case SafeCreationStatus.AWAITING: + return { + image: loading, + description: 'Waiting for transaction confirmation.', + instruction: 'Please confirm the transaction with your connected wallet.', + } + case SafeCreationStatus.WALLET_REJECTED: + return { + image: error, + description: 'Transaction was rejected.', + instruction: 'You can cancel or retry the Safe creation process.', + } + case SafeCreationStatus.PROCESSING: + return { + image: loading, + description: 'Transaction is being executed.', + instruction: 'Please do not leave the page.', + } + case SafeCreationStatus.ERROR: + return { + image: error, + description: 'There was an error.', + instruction: 'You can cancel or retry the Safe creation process.', + } + case SafeCreationStatus.REVERTED: + return { + image: error, + description: 'Transaction was reverted.', + instruction: 'You can cancel or retry the Safe creation process.', + } + case SafeCreationStatus.TIMEOUT: + return { + image: error, + description: 'Transaction was not found. Be aware that it might still be processed.', + instruction: 'You can cancel or retry the Safe creation process.', + } + case SafeCreationStatus.SUCCESS: + return { + image: loading, + description: 'Your Safe was successfully created!', + instruction: 'It is now being indexed. Please do not leave the page.', + } + case SafeCreationStatus.INDEXED: + return { + image: indexed, + description: 'Your Safe was successfully indexed!', + instruction: 'Taking you to your dashboard...', + } + case SafeCreationStatus.INDEX_FAILED: + return { + image: error, + description: 'Your Safe is created and will be indexed by our services shortly.', + instruction: + 'You can already open your Safe. It might take a moment until it becomes fully usable in the interface.', + } + } +} + +const StatusMessage = ({ status }: { status: SafeCreationStatus }) => { + const stepInfo = getStep(status) + + return ( + <> + + {stepInfo.image} + + {stepInfo.description} + + + ({ backgroundColor: palette.warning.background, borderRadius: '6px' })} + padding={3} + mb={3} + > + {stepInfo.instruction} + + + ) +} + +export default StatusMessage diff --git a/src/components/new-safe/steps/Step4/StatusStep.tsx b/src/components/new-safe/steps/Step4/StatusStep.tsx new file mode 100644 index 0000000000..7d68272dd2 --- /dev/null +++ b/src/components/new-safe/steps/Step4/StatusStep.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from 'react' +import { Box, Skeleton, StepLabel, SvgIcon } from '@mui/material' +import css from '@/components/new-safe/steps/Step4/styles.module.css' +import CircleIcon from '@mui/icons-material/Circle' +import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined' +import Identicon from '@/components/common/Identicon' + +const StatusStep = ({ + isLoading, + safeAddress, + children, +}: { + isLoading: boolean + safeAddress?: string + children: ReactNode +}) => { + const Icon = isLoading ? CircleOutlinedIcon : CircleIcon + const color = isLoading ? 'border' : 'primary' + + return ( + } + > + (isLoading ? palette.border.main : palette.text.primary) }} + > + + {safeAddress && !isLoading ? ( + + ) : ( + + )} + + {children} + + + ) +} + +export default StatusStep diff --git a/src/components/new-safe/steps/Step4/StatusStepper.tsx b/src/components/new-safe/steps/Step4/StatusStepper.tsx new file mode 100644 index 0000000000..2009d96f36 --- /dev/null +++ b/src/components/new-safe/steps/Step4/StatusStepper.tsx @@ -0,0 +1,70 @@ +import { Box, Step, StepConnector, Stepper, Typography } from '@mui/material' +import css from '@/components/new-safe/steps/Step4/styles.module.css' +import EthHashInfo from '@/components/common/EthHashInfo' +import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import type { PendingSafeData } from '@/components/new-safe/steps/Step4/index' +import StatusStep from '@/components/new-safe/steps/Step4/StatusStep' + +const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; status: SafeCreationStatus }) => { + if (!pendingSafe?.safeAddress) return null + + const isSuccess = + status === SafeCreationStatus.SUCCESS || + status === SafeCreationStatus.INDEXED || + status === SafeCreationStatus.INDEX_FAILED + + return ( + }> + + + + + Your Safe address: + + + + + + + + + + Validating Transaction + + {pendingSafe.txHash && ( + + )} + + + + + + + Processing + + + + + + + Safe is ready + + + + + ) +} + +export default StatusStepper diff --git a/src/components/new-safe/steps/Step4/index.tsx b/src/components/new-safe/steps/Step4/index.tsx new file mode 100644 index 0000000000..51451a8cfc --- /dev/null +++ b/src/components/new-safe/steps/Step4/index.tsx @@ -0,0 +1,128 @@ +import { useCallback } from 'react' +import { Box, Button, Divider, Grid, Paper, Tooltip } from '@mui/material' +import { useRouter } from 'next/router' + +import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import Track from '@/components/common/Track' +import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import StatusMessage from '@/components/new-safe/steps/Step4/StatusMessage' +import useWallet from '@/hooks/wallets/useWallet' +import useIsWrongChain from '@/hooks/useIsWrongChain' +import useStatus from '@/components/create-safe/status/useStatus' +import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' +import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' +import type { PendingSafeTx } from '@/components/create-safe/types.d' +import useSafeCreationEffects from '@/components/new-safe/steps/Step4/useSafeCreationEffects' +import { useSafeCreation } from '@/components/new-safe/steps/Step4/useSafeCreation' +import StatusStepper from '@/components/new-safe/steps/Step4/StatusStepper' +import { trackEvent } from '@/services/analytics' +import useChainId from '@/hooks/useChainId' +import { getRedirect } from '@/components/new-safe/steps/Step4/logic' + +export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' + +export type PendingSafeData = NewSafeFormData & { + txHash?: string + tx?: PendingSafeTx + safeAddress?: string + saltNonce: number +} + +export const CreateSafeStatus = ({ setStep }: StepRenderProps) => { + const [status, setStatus] = useStatus() + const [pendingSafe, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + const router = useRouter() + const chainId = useChainId() + const wallet = useWallet() + const isWrongChain = useIsWrongChain() + const isConnected = wallet && !isWrongChain + + const { createSafe } = useSafeCreation(pendingSafe, setPendingSafe, status, setStatus) + + useSafeCreationEffects({ + pendingSafe, + setPendingSafe, + status, + setStatus, + }) + + const onClose = useCallback(() => { + setPendingSafe(undefined) + setStep(0) + }, [setPendingSafe, setStep]) + + const onCreate = useCallback(() => { + setStatus(SafeCreationStatus.AWAITING) + void createSafe() + }, [createSafe, setStatus]) + + const onFinish = useCallback(() => { + trackEvent(CREATE_SAFE_EVENTS.GET_STARTED) + + const { safeAddress } = pendingSafe || {} + + if (safeAddress) { + setPendingSafe(undefined) + router.push(getRedirect(chainId, safeAddress, router.query?.safeViewRedirectURL)) + } + }, [chainId, pendingSafe, router, setPendingSafe]) + + const displaySafeLink = status === SafeCreationStatus.INDEXED || status === SafeCreationStatus.INDEX_FAILED + + const displayActions = + status === SafeCreationStatus.ERROR || + status === SafeCreationStatus.REVERTED || + status === SafeCreationStatus.TIMEOUT || + status === SafeCreationStatus.WALLET_REJECTED + + return ( + + + + {!displayActions && pendingSafe && ( + <> + + + + )} + + {displaySafeLink && ( + <> + + + + + + + + )} + + {displayActions && ( + <> + + + + + + + + + + + + + + + )} + + ) +} diff --git a/src/components/new-safe/steps/Step4/logic/index.ts b/src/components/new-safe/steps/Step4/logic/index.ts new file mode 100644 index 0000000000..edfe5ecb3d --- /dev/null +++ b/src/components/new-safe/steps/Step4/logic/index.ts @@ -0,0 +1,232 @@ +import type { Web3Provider, JsonRpcProvider } from '@ethersproject/providers' +import type Safe from '@gnosis.pm/safe-core-sdk' +import { SafeFactory, type DeploySafeProps } from '@gnosis.pm/safe-core-sdk' +import { createEthersAdapter } from '@/hooks/coreSDK/safeCoreSDK' +import type { ChainInfo, SafeInfo } from '@gnosis.pm/safe-react-gateway-sdk' +import { EMPTY_DATA, ZERO_ADDRESS } from '@gnosis.pm/safe-core-sdk/dist/src/utils/constants' +import { + getFallbackHandlerContractInstance, + getGnosisSafeContractInstance, + getProxyFactoryContractInstance, +} from '@/services/contracts/safeContracts' +import { LATEST_SAFE_VERSION } from '@/config/constants' +import type { PredictSafeProps } from '@gnosis.pm/safe-core-sdk/dist/src/safeFactory' +import type { ConnectedWallet } from '@/services/onboard' +import { BigNumber } from '@ethersproject/bignumber' +import { getSafeInfo } from '@gnosis.pm/safe-react-gateway-sdk' +import { backOff } from 'exponential-backoff' +import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { didRevert, type EthersError } from '@/utils/ethers-utils' +import { Errors, logError } from '@/services/exceptions' +import { ErrorCode } from '@ethersproject/logger' +import { isWalletRejection } from '@/utils/wallets' +import type { PendingSafeTx } from '@/components/create-safe/types' +import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' +import type { UrlObject } from 'url' +import chains from '@/config/chains' +import { AppRoutes } from '@/config/routes' +import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' + +/** + * Prepare data for creating a Safe for the Core SDK + */ +export const getSafeDeployProps = ( + safeParams: SafeCreationProps, + callback: (txHash: string) => void, + chainId: string, +): PredictSafeProps & { callback: DeploySafeProps['callback'] } => { + const fallbackHandler = getFallbackHandlerContractInstance(chainId) + + return { + safeAccountConfig: { + threshold: safeParams.threshold, + owners: safeParams.owners, + fallbackHandler: fallbackHandler.address, + }, + safeDeploymentConfig: { + saltNonce: safeParams.saltNonce.toString(), + }, + callback, + } +} + +/** + * Create a Safe creation transaction via Core SDK and submits it to the wallet + */ +export const createNewSafe = async (ethersProvider: Web3Provider, props: DeploySafeProps): Promise => { + const ethAdapter = createEthersAdapter(ethersProvider) + + const safeFactory = await SafeFactory.create({ ethAdapter }) + return safeFactory.deploySafe(props) +} + +/** + * Compute the new counterfactual Safe address before it is actually created + */ +export const computeNewSafeAddress = async (ethersProvider: Web3Provider, props: PredictSafeProps): Promise => { + const ethAdapter = createEthersAdapter(ethersProvider) + + const safeFactory = await SafeFactory.create({ ethAdapter }) + return safeFactory.predictSafeAddress(props) +} + +/** + * Encode a Safe creation transaction NOT using the Core SDK because it doesn't support that + * This is used for gas estimation. + */ +export const encodeSafeCreationTx = ({ + owners, + threshold, + saltNonce, + chain, +}: SafeCreationProps & { chain: ChainInfo }) => { + const safeContract = getGnosisSafeContractInstance(chain, LATEST_SAFE_VERSION) + const proxyContract = getProxyFactoryContractInstance(chain.chainId) + const fallbackHandlerContract = getFallbackHandlerContractInstance(chain.chainId) + + const setupData = safeContract.encode('setup', [ + owners, + threshold, + ZERO_ADDRESS, + EMPTY_DATA, + fallbackHandlerContract.address, + ZERO_ADDRESS, + '0', + ZERO_ADDRESS, + ]) + + return proxyContract.encode('createProxyWithNonce', [safeContract.getAddress(), setupData, saltNonce]) +} + +/** + * Encode a Safe creation tx in a way that we can store locally and monitor using _waitForTransaction + */ +export const getSafeCreationTxInfo = async ( + provider: Web3Provider, + params: NewSafeFormData, + chain: ChainInfo, + saltNonce: number, + wallet: ConnectedWallet, +): Promise => { + const proxyContract = getProxyFactoryContractInstance(chain.chainId) + + const data = encodeSafeCreationTx({ + owners: params.owners.map((owner) => owner.address), + threshold: params.threshold, + saltNonce, + chain, + }) + + return { + data, + from: wallet.address, + nonce: await provider.getTransactionCount(wallet.address), + to: proxyContract.getAddress(), + value: BigNumber.from(0), + startBlock: await provider.getBlockNumber(), + } +} + +export type SafeCreationProps = { + owners: string[] + threshold: number + saltNonce: number +} + +export const estimateSafeCreationGas = async ( + chain: ChainInfo, + provider: JsonRpcProvider, + from: string, + safeParams: SafeCreationProps, +): Promise => { + const proxyFactoryContract = getProxyFactoryContractInstance(chain.chainId) + const encodedSafeCreationTx = encodeSafeCreationTx({ ...safeParams, chain }) + + return provider.estimateGas({ + from: from, + to: proxyFactoryContract.getAddress(), + data: encodedSafeCreationTx, + }) +} + +export const pollSafeInfo = async (chainId: string, safeAddress: string): Promise => { + // exponential delay between attempts for around 4 min + return backOff(() => getSafeInfo(chainId, safeAddress), { + startingDelay: 750, + maxDelay: 20000, + numOfAttempts: 19, + retry: (e) => { + console.info('waiting for client-gateway to provide safe information', e) + return true + }, + }) +} + +export const handleSafeCreationError = (error: EthersError) => { + logError(Errors._800, error.message) + + if (isWalletRejection(error)) { + return SafeCreationStatus.WALLET_REJECTED + } + + if (error.code === ErrorCode.TRANSACTION_REPLACED) { + if (error.reason === 'cancelled') { + return SafeCreationStatus.ERROR + } else { + return SafeCreationStatus.SUCCESS + } + } + + return SafeCreationStatus.TIMEOUT +} + +export const checkSafeCreationTx = async ( + provider: JsonRpcProvider, + pendingTx: PendingSafeTx, + txHash: string, +): Promise => { + const TIMEOUT_TIME = 6.5 * 60 * 1000 // 6.5 minutes + + try { + const receipt = await provider._waitForTransaction(txHash, 1, TIMEOUT_TIME, pendingTx) + + if (didRevert(receipt)) { + return SafeCreationStatus.REVERTED + } + + return SafeCreationStatus.SUCCESS + } catch (err) { + return handleSafeCreationError(err as EthersError) + } +} + +export const getRedirect = ( + chainId: string, + safeAddress: string, + redirectQuery?: string | string[], +): UrlObject | string => { + const redirectUrl = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery + const chainPrefix = Object.keys(chains).find((prefix) => chains[prefix] === chainId) + const address = `${chainPrefix}:${safeAddress}` + + // Should never happen in practice + if (!chainPrefix) return AppRoutes.index + + // Go to the dashboard if no specific redirect is provided + if (!redirectUrl) { + return { pathname: AppRoutes.home, query: { safe: address } } + } + + // Otherwise, redirect to the provided URL (e.g. from a Safe App) + + // Track the redirect to Safe App + if (redirectUrl.includes('apps')) { + trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) + } + + // We're prepending the safe address directly here because the `router.push` doesn't parse + // The URL for already existing query params + const hasQueryParams = redirectUrl.includes('?') + const appendChar = hasQueryParams ? '&' : '?' + return redirectUrl + `${appendChar}safe=${address}` +} diff --git a/src/components/new-safe/steps/Step4/styles.module.css b/src/components/new-safe/steps/Step4/styles.module.css new file mode 100644 index 0000000000..ad0717d2b6 --- /dev/null +++ b/src/components/new-safe/steps/Step4/styles.module.css @@ -0,0 +1,18 @@ +.icon { + width: 12px; + height: 12px; +} + +.connector { + margin-left: 6px; + padding: 0; +} + +.connector :global .MuiStepConnector-line { + border-color: var(--color-border-light); +} + +.label { + padding: 0; + gap: var(--space-2); +} diff --git a/src/components/new-safe/steps/Step4/useSafeCreation.ts b/src/components/new-safe/steps/Step4/useSafeCreation.ts new file mode 100644 index 0000000000..94290ac3a1 --- /dev/null +++ b/src/components/new-safe/steps/Step4/useSafeCreation.ts @@ -0,0 +1,100 @@ +import type { Dispatch, SetStateAction } from 'react' +import { useCallback, useEffect, useState } from 'react' +import { useWeb3 } from '@/hooks/wallets/web3' +import { useCurrentChain } from '@/hooks/useChains' +import useWallet from '@/hooks/wallets/useWallet' +import type { EthersError } from '@/utils/ethers-utils' +import type { PendingSafeData } from '@/components/new-safe/steps/Step4/index' +import type { PendingSafeTx } from '@/components/create-safe/types' +import { + checkSafeCreationTx, + createNewSafe, + getSafeCreationTxInfo, + getSafeDeployProps, + handleSafeCreationError, +} from '@/components/new-safe/steps/Step4/logic' + +export enum SafeCreationStatus { + AWAITING = 'AWAITING', + WALLET_REJECTED = 'WALLET_REJECTED', + PROCESSING = 'PROCESSING', + ERROR = 'ERROR', + REVERTED = 'REVERTED', + TIMEOUT = 'TIMEOUT', + SUCCESS = 'SUCCESS', + INDEXED = 'INDEXED', + INDEX_FAILED = 'INDEX_FAILED', +} + +export const useSafeCreation = ( + pendingSafe: PendingSafeData | undefined, + setPendingSafe: Dispatch>, + status: SafeCreationStatus, + setStatus: Dispatch>, +) => { + const [isCreating, setIsCreating] = useState(false) + const [isWatching, setIsWatching] = useState(false) + + const wallet = useWallet() + const provider = useWeb3() + const chain = useCurrentChain() + + const createSafeCallback = useCallback( + async (txHash: string, tx: PendingSafeTx) => { + setPendingSafe((prev) => (prev ? { ...prev, txHash, tx } : undefined)) + }, + [setPendingSafe], + ) + + const createSafe = useCallback(async () => { + if (!pendingSafe || !provider || !chain || !wallet || isCreating) return + + setIsCreating(true) + + try { + const tx = await getSafeCreationTxInfo(provider, pendingSafe, chain, pendingSafe.saltNonce, wallet) + + const safeParams = getSafeDeployProps( + { + threshold: pendingSafe.threshold, + owners: pendingSafe.owners.map((owner) => owner.address), + saltNonce: pendingSafe.saltNonce, + }, + (txHash) => createSafeCallback(txHash, tx), + chain.chainId, + ) + + await createNewSafe(provider, safeParams) + } catch (err) { + setStatus(handleSafeCreationError(err as EthersError)) + } + + setIsCreating(false) + }, [chain, createSafeCallback, isCreating, pendingSafe, provider, setStatus, wallet]) + + const watchSafeTx = useCallback(async () => { + if (!pendingSafe?.tx || !pendingSafe?.txHash || !provider || isWatching) return + + setStatus(SafeCreationStatus.PROCESSING) + setIsWatching(true) + + const txStatus = await checkSafeCreationTx(provider, pendingSafe.tx, pendingSafe.txHash) + setStatus(txStatus) + setIsWatching(false) + }, [isWatching, pendingSafe, provider, setStatus]) + + useEffect(() => { + if (status !== SafeCreationStatus.AWAITING) return + + if (pendingSafe?.txHash) { + void watchSafeTx() + return + } + + void createSafe() + }, [createSafe, watchSafeTx, pendingSafe?.txHash, status]) + + return { + createSafe, + } +} diff --git a/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts b/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts new file mode 100644 index 0000000000..b7bca942c5 --- /dev/null +++ b/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts @@ -0,0 +1,68 @@ +import type { Dispatch, SetStateAction } from 'react' +import { useEffect } from 'react' +import { pollSafeInfo } from '@/components/create-safe/logic' +import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' +import { updateAddressBook } from '@/components/create-safe/logic/address-book' +import { useAppDispatch } from '@/store' +import useChainId from '@/hooks/useChainId' +import type { PendingSafeData } from '@/components/new-safe/steps/Step4/index' + +const useSafeCreationEffects = ({ + pendingSafe, + setPendingSafe, + status, + setStatus, +}: { + pendingSafe: PendingSafeData | undefined + setPendingSafe: Dispatch> + status: SafeCreationStatus + setStatus: Dispatch> +}) => { + const dispatch = useAppDispatch() + const chainId = useChainId() + + useEffect(() => { + if (status === SafeCreationStatus.SUCCESS) { + trackEvent(CREATE_SAFE_EVENTS.CREATED_SAFE) + + // Add the Safe and add names to the address book + if (pendingSafe && pendingSafe.safeAddress) { + dispatch( + updateAddressBook( + chainId, + pendingSafe.safeAddress, + pendingSafe.name, + pendingSafe.owners, + pendingSafe.threshold, + ), + ) + } + + // Asynchronously wait for Safe creation + if (pendingSafe?.safeAddress) { + pollSafeInfo(chainId, pendingSafe.safeAddress) + .then(() => setStatus(SafeCreationStatus.INDEXED)) + .catch(() => setStatus(SafeCreationStatus.INDEX_FAILED)) + } + return + } + + if (status === SafeCreationStatus.WALLET_REJECTED) { + trackEvent(CREATE_SAFE_EVENTS.REJECT_CREATE_SAFE) + } + + if ( + status === SafeCreationStatus.WALLET_REJECTED || + status === SafeCreationStatus.ERROR || + status === SafeCreationStatus.REVERTED + ) { + if (pendingSafe?.txHash) { + setPendingSafe((prev) => (prev ? { ...prev, txHash: undefined, tx: undefined } : undefined)) + } + return + } + }, [chainId, dispatch, pendingSafe, setPendingSafe, setStatus, status]) +} + +export default useSafeCreationEffects From 69a1b6ba26ca6b8d80ac89e348faec1a4fb4e153 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Tue, 1 Nov 2022 14:11:31 +0100 Subject: [PATCH 2/8] style: Add animated loading spinner --- .../steps/Step4/LoadingSpinner/index.tsx | 33 ++++++ .../Step4/LoadingSpinner/styles.module.css | 111 ++++++++++++++++++ .../new-safe/steps/Step4/StatusMessage.tsx | 22 +--- 3 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx create mode 100644 src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx new file mode 100644 index 0000000000..ba23629d81 --- /dev/null +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx @@ -0,0 +1,33 @@ +import { Box } from '@mui/material' +import css from './styles.module.css' +import classnames from 'classnames' +import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' + +const LoadingSpinner = ({ status }: { status: SafeCreationStatus }) => { + const isError = + status === SafeCreationStatus.ERROR || + status === SafeCreationStatus.REVERTED || + status === SafeCreationStatus.TIMEOUT || + status === SafeCreationStatus.WALLET_REJECTED + + return ( + +
+
+
+
+ + + + + + + + + + + + ) +} + +export default LoadingSpinner diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css new file mode 100644 index 0000000000..d1f24c6296 --- /dev/null +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css @@ -0,0 +1,111 @@ +.box { + width: 100px; + height: 100px; + margin: auto; + position: relative; + filter: url('#gooey'); +} + +.rectError .rect { + background-color: #ff5f72; + animation-play-state: paused; +} + +.rect { + position: absolute; + left: 0; + top: 0; + width: 40px; + height: 40px; + background-color: #12ff80; + transition: background-color 0.1s; +} + +.rectTl { + animation: rect-anim-tl ease-in-out 4s infinite; + animation-delay: 0.1s; +} + +.rectTr { + animation: rect-anim-tr ease-in-out 4s infinite; +} + +.rectBl { + animation: rect-anim-bl ease-in-out 4s infinite; +} + +.rectBr { + animation: rect-anim-br ease-in-out 4s infinite; +} + +@keyframes rect-anim-tl { + 0% { + transform: translateX(0) translateY(0) scale(1); + } + 25% { + transform: translateX(50px) translateY(0) scale(0.5); + } + 50% { + transform: translateX(50px) translateY(50px) scale(1); + } + 75% { + transform: translateX(0) translateY(50px) scale(0.5); + } + 100% { + transform: translateX(0) translateY(0) scale(1); + } +} + +@keyframes rect-anim-tr { + 0% { + transform: translateX(50px) translateY(0) scale(0.5); + } + 25% { + transform: translateX(50px) translateY(50px) scale(1); + } + 50% { + transform: translateX(0) translateY(50px) scale(0.5); + } + 75% { + transform: translateX(0) translateY(0) scale(1); + } + 100% { + transform: translateX(50px) translateY(0) scale(0.5); + } +} + +@keyframes rect-anim-br { + 0% { + transform: translateX(50px) translateY(50px) scale(1); + } + 25% { + transform: translateX(0) translateY(50px) scale(0.5); + } + 50% { + transform: translateX(0) translateY(0) scale(1); + } + 75% { + transform: translateX(50px) translateY(0) scale(0.5); + } + 100% { + transform: translateX(50px) translateY(50px) scale(1); + } +} + +@keyframes rect-anim-bl { + 0% { + transform: translateX(0) translateY(50px) scale(0.5); + } + 25% { + transform: translateX(0) translateY(0) scale(1); + } + 50% { + transform: translateX(50px) translateY(0) scale(0.5); + } + 75% { + transform: translateX(50px) translateY(50px) scale(1); + } + 100% { + transform: translateX(0) translateY(50px) scale(0.5); + } +} diff --git a/src/components/new-safe/steps/Step4/StatusMessage.tsx b/src/components/new-safe/steps/Step4/StatusMessage.tsx index c362924a6c..465d368110 100644 --- a/src/components/new-safe/steps/Step4/StatusMessage.tsx +++ b/src/components/new-safe/steps/Step4/StatusMessage.tsx @@ -1,65 +1,51 @@ import { Box, Typography } from '@mui/material' import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' -import css from '@/components/create-safe/status/styles.module.css' -import classNames from 'classnames' +import LoadingSpinner from '@/components/new-safe/steps/Step4/LoadingSpinner' const getStep = (status: SafeCreationStatus) => { - const loading = Safe logo - const indexed = Safe logo - const error = Safe logo - switch (status) { case SafeCreationStatus.AWAITING: return { - image: loading, - description: 'Waiting for transaction confirmation.', + description: 'Step 1/2: Waiting for transaction confirmation.', instruction: 'Please confirm the transaction with your connected wallet.', } case SafeCreationStatus.WALLET_REJECTED: return { - image: error, description: 'Transaction was rejected.', instruction: 'You can cancel or retry the Safe creation process.', } case SafeCreationStatus.PROCESSING: return { - image: loading, - description: 'Transaction is being executed.', + description: 'Step 2/2: Transaction is being executed.', instruction: 'Please do not leave the page.', } case SafeCreationStatus.ERROR: return { - image: error, description: 'There was an error.', instruction: 'You can cancel or retry the Safe creation process.', } case SafeCreationStatus.REVERTED: return { - image: error, description: 'Transaction was reverted.', instruction: 'You can cancel or retry the Safe creation process.', } case SafeCreationStatus.TIMEOUT: return { - image: error, description: 'Transaction was not found. Be aware that it might still be processed.', instruction: 'You can cancel or retry the Safe creation process.', } case SafeCreationStatus.SUCCESS: return { - image: loading, description: 'Your Safe was successfully created!', instruction: 'It is now being indexed. Please do not leave the page.', } case SafeCreationStatus.INDEXED: return { - image: indexed, description: 'Your Safe was successfully indexed!', instruction: 'Taking you to your dashboard...', } case SafeCreationStatus.INDEX_FAILED: return { - image: error, description: 'Your Safe is created and will be indexed by our services shortly.', instruction: 'You can already open your Safe. It might take a moment until it becomes fully usable in the interface.', @@ -73,7 +59,7 @@ const StatusMessage = ({ status }: { status: SafeCreationStatus }) => { return ( <> - {stepInfo.image} + {stepInfo.description} From 4c915426b065f22b6051b545caa455354ed74150 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 2 Nov 2022 10:59:07 +0100 Subject: [PATCH 3/8] fix: Simplify conditions --- src/components/create-safe/status/useSafeCreation.ts | 2 +- src/components/new-safe/steps/Step4/StatusStepper.tsx | 7 +------ src/components/new-safe/steps/Step4/index.tsx | 9 ++------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/components/create-safe/status/useSafeCreation.ts b/src/components/create-safe/status/useSafeCreation.ts index 4353c13d43..eadbbdcd20 100644 --- a/src/components/create-safe/status/useSafeCreation.ts +++ b/src/components/create-safe/status/useSafeCreation.ts @@ -15,8 +15,8 @@ import type { EthersError } from '@/utils/ethers-utils' export enum SafeCreationStatus { AWAITING = 'AWAITING', - WALLET_REJECTED = 'WALLET_REJECTED', PROCESSING = 'PROCESSING', + WALLET_REJECTED = 'WALLET_REJECTED', ERROR = 'ERROR', REVERTED = 'REVERTED', TIMEOUT = 'TIMEOUT', diff --git a/src/components/new-safe/steps/Step4/StatusStepper.tsx b/src/components/new-safe/steps/Step4/StatusStepper.tsx index 2009d96f36..9cc5e17afc 100644 --- a/src/components/new-safe/steps/Step4/StatusStepper.tsx +++ b/src/components/new-safe/steps/Step4/StatusStepper.tsx @@ -8,11 +8,6 @@ import StatusStep from '@/components/new-safe/steps/Step4/StatusStep' const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; status: SafeCreationStatus }) => { if (!pendingSafe?.safeAddress) return null - const isSuccess = - status === SafeCreationStatus.SUCCESS || - status === SafeCreationStatus.INDEXED || - status === SafeCreationStatus.INDEX_FAILED - return ( }> @@ -50,7 +45,7 @@ const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; - + Processing diff --git a/src/components/new-safe/steps/Step4/index.tsx b/src/components/new-safe/steps/Step4/index.tsx index 51451a8cfc..8f92ac4ca8 100644 --- a/src/components/new-safe/steps/Step4/index.tsx +++ b/src/components/new-safe/steps/Step4/index.tsx @@ -68,13 +68,8 @@ export const CreateSafeStatus = ({ setStep }: StepRenderProps) } }, [chainId, pendingSafe, router, setPendingSafe]) - const displaySafeLink = status === SafeCreationStatus.INDEXED || status === SafeCreationStatus.INDEX_FAILED - - const displayActions = - status === SafeCreationStatus.ERROR || - status === SafeCreationStatus.REVERTED || - status === SafeCreationStatus.TIMEOUT || - status === SafeCreationStatus.WALLET_REJECTED + const displaySafeLink = status >= SafeCreationStatus.INDEXED + const displayActions = status >= SafeCreationStatus.WALLET_REJECTED && status <= SafeCreationStatus.TIMEOUT return ( Date: Wed, 2 Nov 2022 14:20:23 +0100 Subject: [PATCH 4/8] add: animate into css safe logo --- .../steps/Step4/LoadingSpinner/index.tsx | 59 +++++++++++++++++-- .../Step4/LoadingSpinner/styles.module.css | 56 +++++++++++------- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx index ba23629d81..b908950f16 100644 --- a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx @@ -2,6 +2,33 @@ import { Box } from '@mui/material' import css from './styles.module.css' import classnames from 'classnames' import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' +import { createRef, useCallback, useEffect } from 'react' + +const rectTlEndTransform = 'translateX(0) translateY(23px) scaleY(1.25)' +const rectTrEndTransform = 'translateX(25px) scaleX(2)' +const rectBlEndTransform = 'translateX(30px) translateY(60px) scale(1) scaleX(2)' +const rectBrEndTransform = 'translateY(40px) translateX(60px) scaleY(1.25)' + +const moveToEnd = (transformEnd: string, element: HTMLDivElement | null) => { + if (element) { + element.getAnimations().forEach((animation) => { + if ((animation as CSSAnimation).animationName) { + animation.pause() + } + }) + const transformStart = window.getComputedStyle(element).transform + element.getAnimations().forEach((animation) => { + if ((animation as CSSAnimation).animationName) { + animation.cancel() + } + }) + element.animate([{ transform: transformStart }, { transform: transformEnd }], { + duration: 1000, + easing: 'ease-out', + fill: 'forwards', + }) + } +} const LoadingSpinner = ({ status }: { status: SafeCreationStatus }) => { const isError = @@ -10,12 +37,34 @@ const LoadingSpinner = ({ status }: { status: SafeCreationStatus }) => { status === SafeCreationStatus.TIMEOUT || status === SafeCreationStatus.WALLET_REJECTED + const isSuccess = status === SafeCreationStatus.SUCCESS + + const rectTl = createRef() + const rectTr = createRef() + const rectBl = createRef() + const rectBr = createRef() + const rectCenter = createRef() + + const onFinish = useCallback(() => { + moveToEnd(rectTlEndTransform, rectTl.current) + moveToEnd(rectTrEndTransform, rectTr.current) + moveToEnd(rectBlEndTransform, rectBl.current) + moveToEnd(rectBrEndTransform, rectBr.current) + }, [rectBl, rectBr, rectTl, rectTr]) + + useEffect(() => { + if (isSuccess) { + onFinish() + } + }, [isSuccess, onFinish]) + return ( - -
-
-
-
+ +
+
+
+
+
diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css index d1f24c6296..45c6d6497a 100644 --- a/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css @@ -11,12 +11,17 @@ animation-play-state: paused; } +.rectSuccess .rectCenter { + visibility: visible; + transform: translateY(30px) translateX(30px) scale(1); +} + .rect { position: absolute; left: 0; top: 0; - width: 40px; - height: 40px; + width: 20px; + height: 20px; background-color: #12ff80; transition: background-color 0.1s; } @@ -34,78 +39,85 @@ animation: rect-anim-bl ease-in-out 4s infinite; } +.rectCenter { + visibility: hidden; + animation: none; + transition: transform 1s ease-out; + transform: translateY(30px) translateX(30px) scale(0); +} + .rectBr { animation: rect-anim-br ease-in-out 4s infinite; } @keyframes rect-anim-tl { 0% { - transform: translateX(0) translateY(0) scale(1); + transform: translateX(0) translateY(0) scale(2); } 25% { - transform: translateX(50px) translateY(0) scale(0.5); + transform: translateX(50px) translateY(0) scale(1); } 50% { - transform: translateX(50px) translateY(50px) scale(1); + transform: translateX(50px) translateY(50px) scale(2); } 75% { - transform: translateX(0) translateY(50px) scale(0.5); + transform: translateX(0) translateY(50px) scale(1); } 100% { - transform: translateX(0) translateY(0) scale(1); + transform: translateX(0) translateY(0) scale(2); } } @keyframes rect-anim-tr { 0% { - transform: translateX(50px) translateY(0) scale(0.5); + transform: translateX(50px) translateY(0) scale(1); } 25% { - transform: translateX(50px) translateY(50px) scale(1); + transform: translateX(50px) translateY(50px) scale(2); } 50% { - transform: translateX(0) translateY(50px) scale(0.5); + transform: translateX(0) translateY(50px) scale(1); } 75% { - transform: translateX(0) translateY(0) scale(1); + transform: translateX(0) translateY(0) scale(2); } 100% { - transform: translateX(50px) translateY(0) scale(0.5); + transform: translateX(50px) translateY(0) scale(1); } } @keyframes rect-anim-br { 0% { - transform: translateX(50px) translateY(50px) scale(1); + transform: translateX(50px) translateY(50px) scale(2); } 25% { - transform: translateX(0) translateY(50px) scale(0.5); + transform: translateX(0) translateY(50px) scale(1); } 50% { - transform: translateX(0) translateY(0) scale(1); + transform: translateX(0) translateY(0) scale(2); } 75% { - transform: translateX(50px) translateY(0) scale(0.5); + transform: translateX(50px) translateY(0) scale(1); } 100% { - transform: translateX(50px) translateY(50px) scale(1); + transform: translateX(50px) translateY(50px) scale(2); } } @keyframes rect-anim-bl { 0% { - transform: translateX(0) translateY(50px) scale(0.5); + transform: translateX(0) translateY(50px) scale(1); } 25% { - transform: translateX(0) translateY(0) scale(1); + transform: translateX(0) translateY(0) scale(2); } 50% { - transform: translateX(50px) translateY(0) scale(0.5); + transform: translateX(50px) translateY(0) scale(1); } 75% { - transform: translateX(50px) translateY(50px) scale(1); + transform: translateX(50px) translateY(50px) scale(2); } 100% { - transform: translateX(0) translateY(50px) scale(0.5); + transform: translateX(0) translateY(50px) scale(1); } } From d3677884250be053f4c4e67130ee33cd0c1922eb Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 2 Nov 2022 16:25:47 +0100 Subject: [PATCH 5/8] fix: use numeric enum for simpler conditions, adjust animated logo sizes --- .../new-safe/CreateSafe/useCreateSafe.ts | 2 +- .../steps/Step4/LoadingSpinner/index.tsx | 17 +++++--------- .../Step4/LoadingSpinner/styles.module.css | 4 ++-- .../new-safe/steps/Step4/StatusMessage.tsx | 22 ++++++++++--------- .../new-safe/steps/Step4/StatusStepper.tsx | 4 +++- src/components/new-safe/steps/Step4/index.tsx | 8 +++---- .../new-safe/steps/Step4/logic/index.ts | 2 +- .../new-safe/steps/Step4/useSafeCreation.ts | 18 +++++++-------- .../steps/Step4/useSafeCreationEffects.ts | 2 +- 9 files changed, 38 insertions(+), 41 deletions(-) diff --git a/src/components/new-safe/CreateSafe/useCreateSafe.ts b/src/components/new-safe/CreateSafe/useCreateSafe.ts index a2fa36984b..02f6eb86c2 100644 --- a/src/components/new-safe/CreateSafe/useCreateSafe.ts +++ b/src/components/new-safe/CreateSafe/useCreateSafe.ts @@ -21,7 +21,7 @@ const useCreateSafe = (setStep: StepRenderProps['setStep']) => // Jump to the status screen if there is already a tx submitted if (pendingSafe) { - setStep(3) + setStep(4) } }, [setStep, isWrongChain, wallet, pendingSafe]) diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx index b908950f16..afd9f25aaf 100644 --- a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx @@ -4,10 +4,10 @@ import classnames from 'classnames' import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' import { createRef, useCallback, useEffect } from 'react' -const rectTlEndTransform = 'translateX(0) translateY(23px) scaleY(1.25)' -const rectTrEndTransform = 'translateX(25px) scaleX(2)' -const rectBlEndTransform = 'translateX(30px) translateY(60px) scale(1) scaleX(2)' -const rectBrEndTransform = 'translateY(40px) translateX(60px) scaleY(1.25)' +const rectTlEndTransform = 'translateX(0) translateY(20px) scaleY(1.1)' +const rectTrEndTransform = 'translateX(30px) scaleX(2.3)' +const rectBlEndTransform = 'translateX(30px) translateY(60px) scaleX(2.3)' +const rectBrEndTransform = 'translateY(40px) translateX(60px) scaleY(1.1)' const moveToEnd = (transformEnd: string, element: HTMLDivElement | null) => { if (element) { @@ -31,13 +31,8 @@ const moveToEnd = (transformEnd: string, element: HTMLDivElement | null) => { } const LoadingSpinner = ({ status }: { status: SafeCreationStatus }) => { - const isError = - status === SafeCreationStatus.ERROR || - status === SafeCreationStatus.REVERTED || - status === SafeCreationStatus.TIMEOUT || - status === SafeCreationStatus.WALLET_REJECTED - - const isSuccess = status === SafeCreationStatus.SUCCESS + const isError = status >= SafeCreationStatus.WALLET_REJECTED && status <= SafeCreationStatus.TIMEOUT + const isSuccess = status >= SafeCreationStatus.SUCCESS const rectTl = createRef() const rectTr = createRef() diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css index 45c6d6497a..4dcb5c0b32 100644 --- a/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/styles.module.css @@ -1,6 +1,6 @@ .box { - width: 100px; - height: 100px; + width: 80px; + height: 80px; margin: auto; position: relative; filter: url('#gooey'); diff --git a/src/components/new-safe/steps/Step4/StatusMessage.tsx b/src/components/new-safe/steps/Step4/StatusMessage.tsx index 465d368110..03839dde2d 100644 --- a/src/components/new-safe/steps/Step4/StatusMessage.tsx +++ b/src/components/new-safe/steps/Step4/StatusMessage.tsx @@ -1,5 +1,5 @@ import { Box, Typography } from '@mui/material' -import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { SafeCreationStatus } from './useSafeCreation' import LoadingSpinner from '@/components/new-safe/steps/Step4/LoadingSpinner' const getStep = (status: SafeCreationStatus) => { @@ -42,7 +42,7 @@ const getStep = (status: SafeCreationStatus) => { case SafeCreationStatus.INDEXED: return { description: 'Your Safe was successfully indexed!', - instruction: 'Taking you to your dashboard...', + instruction: '', } case SafeCreationStatus.INDEX_FAILED: return { @@ -58,19 +58,21 @@ const StatusMessage = ({ status }: { status: SafeCreationStatus }) => { return ( <> - + {stepInfo.description} - ({ backgroundColor: palette.warning.background, borderRadius: '6px' })} - padding={3} - mb={3} - > - {stepInfo.instruction} - + {stepInfo.instruction && ( + ({ backgroundColor: palette.warning.background, borderRadius: '6px' })} + padding={3} + my={3} + > + {stepInfo.instruction} + + )} ) } diff --git a/src/components/new-safe/steps/Step4/StatusStepper.tsx b/src/components/new-safe/steps/Step4/StatusStepper.tsx index 9cc5e17afc..5c38b729a7 100644 --- a/src/components/new-safe/steps/Step4/StatusStepper.tsx +++ b/src/components/new-safe/steps/Step4/StatusStepper.tsx @@ -1,7 +1,7 @@ import { Box, Step, StepConnector, Stepper, Typography } from '@mui/material' import css from '@/components/new-safe/steps/Step4/styles.module.css' import EthHashInfo from '@/components/common/EthHashInfo' -import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' import type { PendingSafeData } from '@/components/new-safe/steps/Step4/index' import StatusStep from '@/components/new-safe/steps/Step4/StatusStep' @@ -20,6 +20,7 @@ const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; address={pendingSafe.safeAddress} hasExplorer showCopyButton + showName={false} shortAddress={false} showAvatar={false} /> @@ -37,6 +38,7 @@ const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; address={pendingSafe.txHash} hasExplorer showCopyButton + showName={false} shortAddress={true} showAvatar={false} /> diff --git a/src/components/new-safe/steps/Step4/index.tsx b/src/components/new-safe/steps/Step4/index.tsx index 8f92ac4ca8..6d2c0119f3 100644 --- a/src/components/new-safe/steps/Step4/index.tsx +++ b/src/components/new-safe/steps/Step4/index.tsx @@ -1,20 +1,18 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import { Box, Button, Divider, Grid, Paper, Tooltip } from '@mui/material' import { useRouter } from 'next/router' -import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' import Track from '@/components/common/Track' import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' import useLocalStorage from '@/services/local-storage/useLocalStorage' import StatusMessage from '@/components/new-safe/steps/Step4/StatusMessage' import useWallet from '@/hooks/wallets/useWallet' import useIsWrongChain from '@/hooks/useIsWrongChain' -import useStatus from '@/components/create-safe/status/useStatus' import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import type { PendingSafeTx } from '@/components/create-safe/types.d' import useSafeCreationEffects from '@/components/new-safe/steps/Step4/useSafeCreationEffects' -import { useSafeCreation } from '@/components/new-safe/steps/Step4/useSafeCreation' +import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/steps/Step4/useSafeCreation' import StatusStepper from '@/components/new-safe/steps/Step4/StatusStepper' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' @@ -30,7 +28,7 @@ export type PendingSafeData = NewSafeFormData & { } export const CreateSafeStatus = ({ setStep }: StepRenderProps) => { - const [status, setStatus] = useStatus() + const [status, setStatus] = useState(SafeCreationStatus.AWAITING) const [pendingSafe, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) const router = useRouter() const chainId = useChainId() diff --git a/src/components/new-safe/steps/Step4/logic/index.ts b/src/components/new-safe/steps/Step4/logic/index.ts index edfe5ecb3d..4467dc53b0 100644 --- a/src/components/new-safe/steps/Step4/logic/index.ts +++ b/src/components/new-safe/steps/Step4/logic/index.ts @@ -15,7 +15,7 @@ import type { ConnectedWallet } from '@/services/onboard' import { BigNumber } from '@ethersproject/bignumber' import { getSafeInfo } from '@gnosis.pm/safe-react-gateway-sdk' import { backOff } from 'exponential-backoff' -import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' import { didRevert, type EthersError } from '@/utils/ethers-utils' import { Errors, logError } from '@/services/exceptions' import { ErrorCode } from '@ethersproject/logger' diff --git a/src/components/new-safe/steps/Step4/useSafeCreation.ts b/src/components/new-safe/steps/Step4/useSafeCreation.ts index 94290ac3a1..f5b05fce7f 100644 --- a/src/components/new-safe/steps/Step4/useSafeCreation.ts +++ b/src/components/new-safe/steps/Step4/useSafeCreation.ts @@ -15,15 +15,15 @@ import { } from '@/components/new-safe/steps/Step4/logic' export enum SafeCreationStatus { - AWAITING = 'AWAITING', - WALLET_REJECTED = 'WALLET_REJECTED', - PROCESSING = 'PROCESSING', - ERROR = 'ERROR', - REVERTED = 'REVERTED', - TIMEOUT = 'TIMEOUT', - SUCCESS = 'SUCCESS', - INDEXED = 'INDEXED', - INDEX_FAILED = 'INDEX_FAILED', + AWAITING = 1, + PROCESSING = 2, + WALLET_REJECTED = 3, + ERROR = 4, + REVERTED = 5, + TIMEOUT = 6, + SUCCESS = 7, + INDEXED = 8, + INDEX_FAILED = 9, } export const useSafeCreation = ( diff --git a/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts b/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts index b7bca942c5..50515a5721 100644 --- a/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts +++ b/src/components/new-safe/steps/Step4/useSafeCreationEffects.ts @@ -1,7 +1,7 @@ import type { Dispatch, SetStateAction } from 'react' import { useEffect } from 'react' import { pollSafeInfo } from '@/components/create-safe/logic' -import { SafeCreationStatus } from '@/components/create-safe/status/useSafeCreation' +import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { updateAddressBook } from '@/components/create-safe/logic/address-book' import { useAppDispatch } from '@/store' From 613c7324d464e9a0144ab42e6bfba8f6c452e557 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Wed, 2 Nov 2022 17:19:55 +0100 Subject: [PATCH 6/8] fix: Display dialog after safe creation --- .../dashboard/CreationDialog/index.tsx | 76 +++++++++++++++++++ src/components/dashboard/index.tsx | 34 +++++---- .../new-safe/steps/Step4/logic/index.ts | 2 +- 3 files changed, 98 insertions(+), 14 deletions(-) create mode 100644 src/components/dashboard/CreationDialog/index.tsx diff --git a/src/components/dashboard/CreationDialog/index.tsx b/src/components/dashboard/CreationDialog/index.tsx new file mode 100644 index 0000000000..1fbb27182d --- /dev/null +++ b/src/components/dashboard/CreationDialog/index.tsx @@ -0,0 +1,76 @@ +import React, { type ElementType } from 'react' +import { Box, Button, Dialog, DialogContent, Grid, SvgIcon, Typography } from '@mui/material' + +import HomeIcon from '@/public/images/sidebar/home.svg' +import TransactionIcon from '@/public/images/sidebar/transactions.svg' +import AppsIcon from '@/public/images/sidebar/apps.svg' +import SettingsIcon from '@/public/images/sidebar/settings.svg' +import BeamerIcon from '@/public/images/sidebar/whats-new.svg' +import HelpCenterIcon from '@/public/images/sidebar/help-center.svg' + +const HintItem = ({ Icon, title, description }: { Icon: ElementType; title: string; description: string }) => { + return ( + + + + + {title} + + + + {description} + + ) +} + +const CreationDialog = () => { + const [open, setOpen] = React.useState(true) + + return ( + + + + Welcome to your Safe! + + + Congratulations on creating the safest wallet in web3. Keep your assets safe and discover our app. + + + + + + + + + + + + + + + ) +} + +export default CreationDialog diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index 07f5009b28..d99a6bb4ea 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -4,26 +4,34 @@ import PendingTxsList from '@/components/dashboard/PendingTxs/PendingTxsList' import Overview from '@/components/dashboard/Overview/Overview' import { FeaturedApps } from '@/components/dashboard/FeaturedApps/FeaturedApps' import SafeAppsDashboardSection from '@/components/dashboard/SafeAppsDashboardSection/SafeAppsDashboardSection' +import CreationDialog from '@/components/dashboard/CreationDialog' +import { useRouter } from 'next/router' const Dashboard = (): ReactElement => { + const router = useRouter() + const { creation = '' } = router.query + return ( - - - - + <> + + + + - - - + + + - - - + + + - - + + + - + {creation ? : null} + ) } diff --git a/src/components/new-safe/steps/Step4/logic/index.ts b/src/components/new-safe/steps/Step4/logic/index.ts index 4467dc53b0..4121ab8264 100644 --- a/src/components/new-safe/steps/Step4/logic/index.ts +++ b/src/components/new-safe/steps/Step4/logic/index.ts @@ -214,7 +214,7 @@ export const getRedirect = ( // Go to the dashboard if no specific redirect is provided if (!redirectUrl) { - return { pathname: AppRoutes.home, query: { safe: address } } + return { pathname: AppRoutes.home, query: { safe: address, creation: 1 } } } // Otherwise, redirect to the provided URL (e.g. from a Safe App) From 6700a5f3a2a2f616cc972afc0be8ac5f94ea484b Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 4 Nov 2022 11:30:39 +0100 Subject: [PATCH 7/8] fix: Feedback --- .../status/useSafeCreationEffects.ts | 8 +++- .../dashboard/CreationDialog/index.tsx | 6 +-- src/components/new-safe/CreateSafe/index.tsx | 2 +- ...useCreateSafe.ts => useSetCreationStep.ts} | 16 ++----- src/components/new-safe/steps/Step0/index.tsx | 6 ++- src/components/new-safe/steps/Step1/index.tsx | 6 ++- src/components/new-safe/steps/Step2/index.tsx | 6 ++- src/components/new-safe/steps/Step3/index.tsx | 6 ++- .../steps/Step4/LoadingSpinner/index.tsx | 12 +++--- .../new-safe/steps/Step4/StatusMessage.tsx | 8 ++-- src/components/new-safe/steps/Step4/index.tsx | 2 +- .../new-safe/steps/Step4/logic/index.ts | 35 ---------------- .../new-safe/steps/Step4/useSafeCreation.ts | 22 +++++----- src/hooks/__tests__/useIsConnected.ts | 42 +++++++++++++++++++ src/hooks/useIsConnected.ts | 12 ++++++ 15 files changed, 107 insertions(+), 82 deletions(-) rename src/components/new-safe/CreateSafe/{useCreateSafe.ts => useSetCreationStep.ts} (61%) create mode 100644 src/hooks/__tests__/useIsConnected.ts create mode 100644 src/hooks/useIsConnected.ts diff --git a/src/components/create-safe/status/useSafeCreationEffects.ts b/src/components/create-safe/status/useSafeCreationEffects.ts index 9da3db167c..d0b744844d 100644 --- a/src/components/create-safe/status/useSafeCreationEffects.ts +++ b/src/components/create-safe/status/useSafeCreationEffects.ts @@ -12,7 +12,11 @@ import { useAppDispatch } from '@/store' import type { PendingSafeData } from '@/components/create-safe/types.d' import useChainId from '@/hooks/useChainId' -const getRedirect = (chainId: string, safeAddress: string, redirectQuery?: string | string[]): UrlObject | string => { +export const getRedirect = ( + chainId: string, + safeAddress: string, + redirectQuery?: string | string[], +): UrlObject | string => { const redirectUrl = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery const chainPrefix = Object.keys(chains).find((prefix) => chains[prefix] === chainId) const address = `${chainPrefix}:${safeAddress}` @@ -28,12 +32,14 @@ const getRedirect = (chainId: string, safeAddress: string, redirectQuery?: strin // Otherwise, redirect to the provided URL (e.g. from a Safe App) // Track the redirect to Safe App + // TODO: Narrow this down to /apps only if (redirectUrl.includes('apps')) { trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) } // We're prepending the safe address directly here because the `router.push` doesn't parse // The URL for already existing query params + // TODO: Check if we can accomplish this with URLSearchParams or URL instead const hasQueryParams = redirectUrl.includes('?') const appendChar = hasQueryParams ? '&' : '?' return redirectUrl + `${appendChar}safe=${address}` diff --git a/src/components/dashboard/CreationDialog/index.tsx b/src/components/dashboard/CreationDialog/index.tsx index 1fbb27182d..56a828495d 100644 --- a/src/components/dashboard/CreationDialog/index.tsx +++ b/src/components/dashboard/CreationDialog/index.tsx @@ -39,17 +39,17 @@ const CreationDialog = () => { { mobileOwners: [] as NamedAddress[], owners: [defaultOwner], threshold: 1, - saltNonce: 0, + saltNonce: Date.now(), } const onClose = () => { diff --git a/src/components/new-safe/CreateSafe/useCreateSafe.ts b/src/components/new-safe/CreateSafe/useSetCreationStep.ts similarity index 61% rename from src/components/new-safe/CreateSafe/useCreateSafe.ts rename to src/components/new-safe/CreateSafe/useSetCreationStep.ts index 02f6eb86c2..488ef56366 100644 --- a/src/components/new-safe/CreateSafe/useCreateSafe.ts +++ b/src/components/new-safe/CreateSafe/useSetCreationStep.ts @@ -1,5 +1,3 @@ -import useIsWrongChain from '@/hooks/useIsWrongChain' -import useWallet from '@/hooks/wallets/useWallet' import { useEffect } from 'react' import useLocalStorage from '@/services/local-storage/useLocalStorage' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' @@ -7,15 +5,11 @@ import type { PendingSafeData } from '@/components/new-safe/steps/Step4' import type { NewSafeFormData } from '@/components/new-safe/CreateSafe/index' import { SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/steps/Step4' -const useCreateSafe = (setStep: StepRenderProps['setStep']) => { +const useSetCreationStep = (setStep: StepRenderProps['setStep'], isConnected: boolean) => { const [pendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) - const wallet = useWallet() - const isWrongChain = useIsWrongChain() - - const isConnected = wallet && !isWrongChain useEffect(() => { - if (isWrongChain || !wallet) { + if (!isConnected) { setStep(0) } @@ -23,9 +17,7 @@ const useCreateSafe = (setStep: StepRenderProps['setStep']) => if (pendingSafe) { setStep(4) } - }, [setStep, isWrongChain, wallet, pendingSafe]) - - return { isConnected } + }, [isConnected, setStep, pendingSafe]) } -export default useCreateSafe +export default useSetCreationStep diff --git a/src/components/new-safe/steps/Step0/index.tsx b/src/components/new-safe/steps/Step0/index.tsx index 5588886778..78195b8908 100644 --- a/src/components/new-safe/steps/Step0/index.tsx +++ b/src/components/new-safe/steps/Step0/index.tsx @@ -10,8 +10,9 @@ import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import WalletDetails from '@/components/common/ConnectWallet/WalletDetails' import PairingDetails from '@/components/common/PairingDetails' -import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' import type { ConnectedWallet } from '@/services/onboard' +import useIsConnected from '@/hooks/useIsConnected' +import useSetCreationStep from '@/components/new-safe/CreateSafe/useSetCreationStep' export const ConnectWalletContent = ({ onSubmit }: { onSubmit: StepRenderProps['onSubmit'] }) => { const isWrongChain = useIsWrongChain() @@ -57,7 +58,8 @@ export const ConnectWalletContent = ({ onSubmit }: { onSubmit: StepRenderProps) => { - const { isConnected } = useCreateSafe(setStep) + const isConnected = useIsConnected() + useSetCreationStep(setStep, isConnected) return ( diff --git a/src/components/new-safe/steps/Step1/index.tsx b/src/components/new-safe/steps/Step1/index.tsx index 15610f316d..0f9ea1b61e 100644 --- a/src/components/new-safe/steps/Step1/index.tsx +++ b/src/components/new-safe/steps/Step1/index.tsx @@ -19,7 +19,8 @@ import css from './styles.module.css' import NetworkSelector from '@/components/common/NetworkSelector' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' -import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' +import useIsConnected from '@/hooks/useIsConnected' +import useSetCreationStep from '@/components/new-safe/CreateSafe/useSetCreationStep' type CreateSafeStep1Form = { name: string @@ -33,7 +34,8 @@ const STEP_1_FORM_ID = 'create-safe-step-1-form' function CreateSafeStep1({ data, onSubmit, onBack, setStep }: StepRenderProps) { const fallbackName = useMnemonicSafeName() - const { isConnected } = useCreateSafe(setStep) + const isConnected = useIsConnected() + useSetCreationStep(setStep, isConnected) const { handleSubmit, diff --git a/src/components/new-safe/steps/Step2/index.tsx b/src/components/new-safe/steps/Step2/index.tsx index aa77e48f86..3d8013b1ee 100644 --- a/src/components/new-safe/steps/Step2/index.tsx +++ b/src/components/new-safe/steps/Step2/index.tsx @@ -8,7 +8,8 @@ import { OwnerRow } from './OwnerRow' import type { NamedAddress } from '@/components/create-safe/types' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' -import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' +import useIsConnected from '@/hooks/useIsConnected' +import useSetCreationStep from '@/components/new-safe/CreateSafe/useSetCreationStep' type CreateSafeStep2Form = { owners: NamedAddress[] @@ -25,7 +26,8 @@ enum CreateSafeStep2Fields { const STEP_2_FORM_ID = 'create-safe-step-2-form' const CreateSafeStep2 = ({ onSubmit, onBack, data, setStep }: StepRenderProps): ReactElement => { - const { isConnected } = useCreateSafe(setStep) + const isConnected = useIsConnected() + useSetCreationStep(setStep, isConnected) const formMethods = useForm({ mode: 'all', diff --git a/src/components/new-safe/steps/Step3/index.tsx b/src/components/new-safe/steps/Step3/index.tsx index 3865e465d9..dba099eb97 100644 --- a/src/components/new-safe/steps/Step3/index.tsx +++ b/src/components/new-safe/steps/Step3/index.tsx @@ -15,9 +15,10 @@ import { getFallbackHandlerContractInstance } from '@/services/contracts/safeCon import { computeNewSafeAddress } from '@/components/create-safe/logic' import useWallet from '@/hooks/wallets/useWallet' import { useWeb3 } from '@/hooks/wallets/web3' -import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' import useLocalStorage from '@/services/local-storage/useLocalStorage' import { type PendingSafeData, SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/steps/Step4' +import useIsConnected from '@/hooks/useIsConnected' +import useSetCreationStep from '@/components/new-safe/CreateSafe/useSetCreationStep' enum CreateSafeStep3Fields { name = 'name', @@ -46,7 +47,8 @@ const ReviewRow = ({ name, value }: { name: string; value: ReactElement }) => { } const CreateSafeStep3 = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { - const { isConnected } = useCreateSafe(setStep) + const isConnected = useIsConnected() + useSetCreationStep(setStep, isConnected) const chain = useCurrentChain() const wallet = useWallet() const provider = useWeb3() diff --git a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx index afd9f25aaf..2de0501100 100644 --- a/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx +++ b/src/components/new-safe/steps/Step4/LoadingSpinner/index.tsx @@ -2,7 +2,7 @@ import { Box } from '@mui/material' import css from './styles.module.css' import classnames from 'classnames' import { SafeCreationStatus } from '@/components/new-safe/steps/Step4/useSafeCreation' -import { createRef, useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' const rectTlEndTransform = 'translateX(0) translateY(20px) scaleY(1.1)' const rectTrEndTransform = 'translateX(30px) scaleX(2.3)' @@ -34,11 +34,11 @@ const LoadingSpinner = ({ status }: { status: SafeCreationStatus }) => { const isError = status >= SafeCreationStatus.WALLET_REJECTED && status <= SafeCreationStatus.TIMEOUT const isSuccess = status >= SafeCreationStatus.SUCCESS - const rectTl = createRef() - const rectTr = createRef() - const rectBl = createRef() - const rectBr = createRef() - const rectCenter = createRef() + const rectTl = useRef(null) + const rectTr = useRef(null) + const rectBl = useRef(null) + const rectBr = useRef(null) + const rectCenter = useRef(null) const onFinish = useCallback(() => { moveToEnd(rectTlEndTransform, rectTl.current) diff --git a/src/components/new-safe/steps/Step4/StatusMessage.tsx b/src/components/new-safe/steps/Step4/StatusMessage.tsx index 03839dde2d..825e7938ab 100644 --- a/src/components/new-safe/steps/Step4/StatusMessage.tsx +++ b/src/components/new-safe/steps/Step4/StatusMessage.tsx @@ -12,7 +12,7 @@ const getStep = (status: SafeCreationStatus) => { case SafeCreationStatus.WALLET_REJECTED: return { description: 'Transaction was rejected.', - instruction: 'You can cancel or retry the Safe creation process.', + instruction: 'Please cancel or retry the Safe creation process.', } case SafeCreationStatus.PROCESSING: return { @@ -22,17 +22,17 @@ const getStep = (status: SafeCreationStatus) => { case SafeCreationStatus.ERROR: return { description: 'There was an error.', - instruction: 'You can cancel or retry the Safe creation process.', + instruction: 'Please cancel or retry the Safe creation process.', } case SafeCreationStatus.REVERTED: return { description: 'Transaction was reverted.', - instruction: 'You can cancel or retry the Safe creation process.', + instruction: 'Please cancel or retry the Safe creation process.', } case SafeCreationStatus.TIMEOUT: return { description: 'Transaction was not found. Be aware that it might still be processed.', - instruction: 'You can cancel or retry the Safe creation process.', + instruction: 'Please cancel or retry the Safe creation process.', } case SafeCreationStatus.SUCCESS: return { diff --git a/src/components/new-safe/steps/Step4/index.tsx b/src/components/new-safe/steps/Step4/index.tsx index 6d2c0119f3..d3e73518d2 100644 --- a/src/components/new-safe/steps/Step4/index.tsx +++ b/src/components/new-safe/steps/Step4/index.tsx @@ -16,7 +16,7 @@ import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/steps import StatusStepper from '@/components/new-safe/steps/Step4/StatusStepper' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' -import { getRedirect } from '@/components/new-safe/steps/Step4/logic' +import { getRedirect } from '@/components/create-safe/status/useSafeCreationEffects' export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' diff --git a/src/components/new-safe/steps/Step4/logic/index.ts b/src/components/new-safe/steps/Step4/logic/index.ts index 4121ab8264..7c8935b603 100644 --- a/src/components/new-safe/steps/Step4/logic/index.ts +++ b/src/components/new-safe/steps/Step4/logic/index.ts @@ -22,10 +22,6 @@ import { ErrorCode } from '@ethersproject/logger' import { isWalletRejection } from '@/utils/wallets' import type { PendingSafeTx } from '@/components/create-safe/types' import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' -import type { UrlObject } from 'url' -import chains from '@/config/chains' -import { AppRoutes } from '@/config/routes' -import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' /** * Prepare data for creating a Safe for the Core SDK @@ -199,34 +195,3 @@ export const checkSafeCreationTx = async ( return handleSafeCreationError(err as EthersError) } } - -export const getRedirect = ( - chainId: string, - safeAddress: string, - redirectQuery?: string | string[], -): UrlObject | string => { - const redirectUrl = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery - const chainPrefix = Object.keys(chains).find((prefix) => chains[prefix] === chainId) - const address = `${chainPrefix}:${safeAddress}` - - // Should never happen in practice - if (!chainPrefix) return AppRoutes.index - - // Go to the dashboard if no specific redirect is provided - if (!redirectUrl) { - return { pathname: AppRoutes.home, query: { safe: address, creation: 1 } } - } - - // Otherwise, redirect to the provided URL (e.g. from a Safe App) - - // Track the redirect to Safe App - if (redirectUrl.includes('apps')) { - trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) - } - - // We're prepending the safe address directly here because the `router.push` doesn't parse - // The URL for already existing query params - const hasQueryParams = redirectUrl.includes('?') - const appendChar = hasQueryParams ? '&' : '?' - return redirectUrl + `${appendChar}safe=${address}` -} diff --git a/src/components/new-safe/steps/Step4/useSafeCreation.ts b/src/components/new-safe/steps/Step4/useSafeCreation.ts index f5b05fce7f..cc1b9cc9ba 100644 --- a/src/components/new-safe/steps/Step4/useSafeCreation.ts +++ b/src/components/new-safe/steps/Step4/useSafeCreation.ts @@ -15,15 +15,15 @@ import { } from '@/components/new-safe/steps/Step4/logic' export enum SafeCreationStatus { - AWAITING = 1, - PROCESSING = 2, - WALLET_REJECTED = 3, - ERROR = 4, - REVERTED = 5, - TIMEOUT = 6, - SUCCESS = 7, - INDEXED = 8, - INDEX_FAILED = 9, + AWAITING, + PROCESSING, + WALLET_REJECTED, + ERROR, + REVERTED, + TIMEOUT, + SUCCESS, + INDEXED, + INDEX_FAILED, } export const useSafeCreation = ( @@ -32,8 +32,8 @@ export const useSafeCreation = ( status: SafeCreationStatus, setStatus: Dispatch>, ) => { - const [isCreating, setIsCreating] = useState(false) - const [isWatching, setIsWatching] = useState(false) + const [isCreating, setIsCreating] = useState(false) + const [isWatching, setIsWatching] = useState(false) const wallet = useWallet() const provider = useWeb3() diff --git a/src/hooks/__tests__/useIsConnected.ts b/src/hooks/__tests__/useIsConnected.ts new file mode 100644 index 0000000000..7d30f7db6d --- /dev/null +++ b/src/hooks/__tests__/useIsConnected.ts @@ -0,0 +1,42 @@ +import { renderHook } from '@testing-library/react' + +import useIsConnected from '@/hooks/useIsConnected' +import * as wallet from '@/hooks/wallets/useWallet' +import type { ConnectedWallet } from '@/services/onboard' +import * as wrongChain from '@/hooks/useIsWrongChain' + +describe('useIsConnected', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return true if a wallet is connected on the current chain', async () => { + jest.spyOn(wrongChain, 'default').mockReturnValue(false) + jest.spyOn(wallet, 'default').mockReturnValue({ + address: '0x1', + } as ConnectedWallet) + + const { result } = renderHook(() => useIsConnected()) + + expect(result.current).toBe(true) + }) + + it('should return false if no wallet is connected', async () => { + jest.spyOn(wallet, 'default').mockReturnValue(null) + + const { result } = renderHook(() => useIsConnected()) + + expect(result.current).toBe(false) + }) + + it('should return false if a wallet is connected on the wrong chain', async () => { + jest.spyOn(wrongChain, 'default').mockReturnValue(true) + jest.spyOn(wallet, 'default').mockReturnValue({ + address: '0x1', + } as ConnectedWallet) + + const { result } = renderHook(() => useIsConnected()) + + expect(result.current).toBe(false) + }) +}) diff --git a/src/hooks/useIsConnected.ts b/src/hooks/useIsConnected.ts new file mode 100644 index 0000000000..04b0cb2edd --- /dev/null +++ b/src/hooks/useIsConnected.ts @@ -0,0 +1,12 @@ +import useIsWrongChain from '@/hooks/useIsWrongChain' +import useWallet from '@/hooks/wallets/useWallet' + +const useIsConnected = () => { + const wallet = useWallet() + const isWrongChain = useIsWrongChain() + + const isConnected = !!wallet && !isWrongChain + return isConnected +} + +export default useIsConnected From 5d1e68c445c192a30f17d8245c4b8f185208e47a Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 4 Nov 2022 11:54:16 +0100 Subject: [PATCH 8/8] fix: Better name for query param --- .../status/useSafeCreationEffects.ts | 6 +-- src/components/dashboard/index.tsx | 4 +- src/components/new-safe/steps/Step4/index.tsx | 2 +- .../new-safe/steps/Step4/logic/index.ts | 37 +++++++++++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/components/create-safe/status/useSafeCreationEffects.ts b/src/components/create-safe/status/useSafeCreationEffects.ts index d0b744844d..c294c47e56 100644 --- a/src/components/create-safe/status/useSafeCreationEffects.ts +++ b/src/components/create-safe/status/useSafeCreationEffects.ts @@ -12,11 +12,7 @@ import { useAppDispatch } from '@/store' import type { PendingSafeData } from '@/components/create-safe/types.d' import useChainId from '@/hooks/useChainId' -export const getRedirect = ( - chainId: string, - safeAddress: string, - redirectQuery?: string | string[], -): UrlObject | string => { +const getRedirect = (chainId: string, safeAddress: string, redirectQuery?: string | string[]): UrlObject | string => { const redirectUrl = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery const chainPrefix = Object.keys(chains).find((prefix) => chains[prefix] === chainId) const address = `${chainPrefix}:${safeAddress}` diff --git a/src/components/dashboard/index.tsx b/src/components/dashboard/index.tsx index d99a6bb4ea..d73197a1f9 100644 --- a/src/components/dashboard/index.tsx +++ b/src/components/dashboard/index.tsx @@ -9,7 +9,7 @@ import { useRouter } from 'next/router' const Dashboard = (): ReactElement => { const router = useRouter() - const { creation = '' } = router.query + const { showCreationModal = '' } = router.query return ( <> @@ -30,7 +30,7 @@ const Dashboard = (): ReactElement => { - {creation ? : null} + {showCreationModal ? : null} ) } diff --git a/src/components/new-safe/steps/Step4/index.tsx b/src/components/new-safe/steps/Step4/index.tsx index d3e73518d2..6d2c0119f3 100644 --- a/src/components/new-safe/steps/Step4/index.tsx +++ b/src/components/new-safe/steps/Step4/index.tsx @@ -16,7 +16,7 @@ import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/steps import StatusStepper from '@/components/new-safe/steps/Step4/StatusStepper' import { trackEvent } from '@/services/analytics' import useChainId from '@/hooks/useChainId' -import { getRedirect } from '@/components/create-safe/status/useSafeCreationEffects' +import { getRedirect } from '@/components/new-safe/steps/Step4/logic' export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' diff --git a/src/components/new-safe/steps/Step4/logic/index.ts b/src/components/new-safe/steps/Step4/logic/index.ts index 7c8935b603..07e5716a54 100644 --- a/src/components/new-safe/steps/Step4/logic/index.ts +++ b/src/components/new-safe/steps/Step4/logic/index.ts @@ -22,6 +22,10 @@ import { ErrorCode } from '@ethersproject/logger' import { isWalletRejection } from '@/utils/wallets' import type { PendingSafeTx } from '@/components/create-safe/types' import type { NewSafeFormData } from '@/components/new-safe/CreateSafe' +import type { UrlObject } from 'url' +import chains from '@/config/chains' +import { AppRoutes } from '@/config/routes' +import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics' /** * Prepare data for creating a Safe for the Core SDK @@ -195,3 +199,36 @@ export const checkSafeCreationTx = async ( return handleSafeCreationError(err as EthersError) } } + +export const getRedirect = ( + chainId: string, + safeAddress: string, + redirectQuery?: string | string[], +): UrlObject | string => { + const redirectUrl = Array.isArray(redirectQuery) ? redirectQuery[0] : redirectQuery + const chainPrefix = Object.keys(chains).find((prefix) => chains[prefix] === chainId) + const address = `${chainPrefix}:${safeAddress}` + + // Should never happen in practice + if (!chainPrefix) return AppRoutes.index + + // Go to the dashboard if no specific redirect is provided + if (!redirectUrl) { + return { pathname: AppRoutes.home, query: { safe: address, showCreationModal: true } } + } + + // Otherwise, redirect to the provided URL (e.g. from a Safe App) + + // Track the redirect to Safe App + // TODO: Narrow this down to /apps only + if (redirectUrl.includes('apps')) { + trackEvent(SAFE_APPS_EVENTS.SHARED_APP_OPEN_AFTER_SAFE_CREATION) + } + + // We're prepending the safe address directly here because the `router.push` doesn't parse + // The URL for already existing query params + // TODO: Check if we can accomplish this with URLSearchParams or URL instead + const hasQueryParams = redirectUrl.includes('?') + const appendChar = hasQueryParams ? '&' : '?' + return redirectUrl + `${appendChar}safe=${address}` +}