Skip to content

Commit

Permalink
feat: [Counterfactual] Add pay now pay later option to safe creation (#…
Browse files Browse the repository at this point in the history
…3222)

* feat: Add pay now pay later option to safe creation for counterfactual safes

* fix: Add tests for ReviewStep

* fix: Hide fee in pay now if can relay

* fix: Update design for pay now pay later block
  • Loading branch information
usame-algan authored Feb 12, 2024
1 parent 8df393c commit 7842373
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 49 deletions.
12 changes: 7 additions & 5 deletions src/components/new-safe/ReviewRow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { type ReactElement } from 'react'
import { Grid, Typography } from '@mui/material'

const ReviewRow = ({ name, value }: { name: string; value: ReactElement }) => {
const ReviewRow = ({ name, value }: { name?: string; value: ReactElement }) => {
return (
<>
<Grid item xs={3}>
<Typography variant="body2">{name}</Typography>
</Grid>
<Grid item xs={9}>
{name && (
<Grid item xs={3}>
<Typography variant="body2">{name}</Typography>
</Grid>
)}
<Grid item xs={name ? 9 : 12}>
{value}
</Grid>
</>
Expand Down
112 changes: 108 additions & 4 deletions src/components/new-safe/create/steps/ReviewStep/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { NewSafeFormData } from '@/components/new-safe/create'
import * as useChains from '@/hooks/useChains'
import * as relay from '@/utils/relaying'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

import { render } from '@/tests/test-utils'
import { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index'
import ReviewStep, { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index'
import * as useWallet from '@/hooks/wallets/useWallet'
import { type ConnectedWallet } from '@/hooks/wallets/useOnboard'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule'
import * as socialLogin from '@/services/mpc/SocialLoginModule'
import { act, fireEvent } from '@testing-library/react'

const mockChainInfo = {
chainId: '100',
Expand All @@ -25,18 +29,118 @@ describe('NetworkFee', () => {
})

it('displays a sponsored by message for social login', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet)
const result = render(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={true} />)

expect(result.getByText(/Your account is sponsored by Gnosis/)).toBeInTheDocument()
})

it('displays an error message for social login if there are no relays left', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'Social Login' } as unknown as ConnectedWallet)
const result = render(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={false} />)

expect(
result.getByText(/You have used up your 5 free transactions per hour. Please try again later/),
).toBeInTheDocument()
})
})

describe('ReviewStep', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should display a pay now pay later option for counterfactual safe setups', () => {
const mockData: NewSafeFormData = {
name: 'Test',
threshold: 1,
owners: [{ name: '', address: '0x1' }],
saltNonce: 0,
}
jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)

const { getByText } = render(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

expect(getByText('Pay now')).toBeInTheDocument()
})

it('should not display the network fee for counterfactual safes', () => {
const mockData: NewSafeFormData = {
name: 'Test',
threshold: 1,
owners: [{ name: '', address: '0x1' }],
saltNonce: 0,
}
jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)

const { queryByText } = render(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

expect(queryByText('You will have to confirm a transaction and pay an estimated fee')).not.toBeInTheDocument()
})

it('should not display the execution method for counterfactual safes', () => {
const mockData: NewSafeFormData = {
name: 'Test',
threshold: 1,
owners: [{ name: '', address: '0x1' }],
saltNonce: 0,
}
jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)

const { queryByText } = render(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

expect(queryByText('Who will pay gas fees:')).not.toBeInTheDocument()
})

it('should display the network fee for counterfactual safes if the user selects pay now', async () => {
const mockData: NewSafeFormData = {
name: 'Test',
threshold: 1,
owners: [{ name: '', address: '0x1' }],
saltNonce: 0,
}
jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)

const { getByText } = render(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

const payNow = getByText('Pay now')

act(() => {
fireEvent.click(payNow)
})

expect(getByText(/You will have to confirm a transaction and pay an estimated fee/)).toBeInTheDocument()
})

it('should display the execution method for counterfactual safes if the user selects pay now and there is relaying', async () => {
const mockData: NewSafeFormData = {
name: 'Test',
threshold: 1,
owners: [{ name: '', address: '0x1' }],
saltNonce: 0,
}
jest.spyOn(useChains, 'useHasFeature').mockReturnValue(true)
jest.spyOn(relay, 'hasRemainingRelays').mockReturnValue(true)
jest.spyOn(socialLogin, 'isSocialLoginWallet').mockReturnValue(false)

const { getByText } = render(
<ReviewStep data={mockData} onSubmit={jest.fn()} onBack={jest.fn()} setStep={jest.fn()} />,
)

const payNow = getByText('Pay now')

act(() => {
fireEvent.click(payNow)
})

expect(getByText(/Who will pay gas fees:/)).toBeInTheDocument()
})
})
109 changes: 69 additions & 40 deletions src/components/new-safe/create/steps/ReviewStep/index.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,64 @@
import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils'
import type { NamedAddress } from '@/components/new-safe/create/types'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { createCounterfactualSafe } from '@/features/counterfactual/utils'
import useWalletCanPay from '@/hooks/useWalletCanPay'
import { useAppDispatch } from '@/store'
import { FEATURES } from '@/utils/chains'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { Button, Grid, Typography, Divider, Box, Alert } from '@mui/material'
import lightPalette from '@/components/theme/lightPalette'
import ChainIndicator from '@/components/common/ChainIndicator'
import type { NamedAddress } from '@/components/new-safe/create/types'
import EthHashInfo from '@/components/common/EthHashInfo'
import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
import useGasPrice, { getTotalFeeFormatted } from '@/hooks/useGasPrice'
import { useEstimateSafeCreationGas } from '@/components/new-safe/create/useEstimateSafeCreationGas'
import { getTotalFeeFormatted } from '@/hooks/useGasPrice'
import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper'
import type { NewSafeFormData } from '@/components/new-safe/create'
import { computeNewSafeAddress } from '@/components/new-safe/create/logic'
import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils'
import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
import css from '@/components/new-safe/create/steps/ReviewStep/styles.module.css'
import layoutCss from '@/components/new-safe/create/styles.module.css'
import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts'
import { computeNewSafeAddress } from '@/components/new-safe/create/logic'
import useWallet from '@/hooks/wallets/useWallet'
import { useWeb3 } from '@/hooks/wallets/web3'
import { useEstimateSafeCreationGas } from '@/components/new-safe/create/useEstimateSafeCreationGas'
import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import NetworkWarning from '@/components/new-safe/create/NetworkWarning'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import ReviewRow from '@/components/new-safe/ReviewRow'
import { ExecutionMethodSelector, ExecutionMethod } from '@/components/tx/ExecutionMethodSelector'
import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays'
import classnames from 'classnames'
import { hasRemainingRelays } from '@/utils/relaying'
import { usePendingSafe } from '../StatusStep/usePendingSafe'
import ErrorMessage from '@/components/tx/ErrorMessage'
import { ExecutionMethod, ExecutionMethodSelector } from '@/components/tx/ExecutionMethodSelector'
import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy'
import { LATEST_SAFE_VERSION } from '@/config/constants'
import PayNowPayLater, { PayMethod } from '@/features/counterfactual/PayNowPayLater'
import { createCounterfactualSafe } from '@/features/counterfactual/utils'
import { useCurrentChain, useHasFeature } from '@/hooks/useChains'
import useGasPrice from '@/hooks/useGasPrice'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import { MAX_HOUR_RELAYS, useLeastRemainingRelays } from '@/hooks/useRemainingRelays'
import useWalletCanPay from '@/hooks/useWalletCanPay'
import useWallet from '@/hooks/wallets/useWallet'
import { useWeb3 } from '@/hooks/wallets/web3'
import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeContracts'
import { isSocialLoginWallet } from '@/services/mpc/SocialLoginModule'
import { RELAY_SPONSORS } from '@/components/tx/SponsoredBy'
import Image from 'next/image'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { useAppDispatch } from '@/store'
import { FEATURES } from '@/utils/chains'
import { hasRemainingRelays } from '@/utils/relaying'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import { Alert, Box, Button, Divider, Grid, Typography } from '@mui/material'
import { type DeploySafeProps } from '@safe-global/protocol-kit'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import classnames from 'classnames'
import Image from 'next/image'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { usePendingSafe } from '../StatusStep/usePendingSafe'

export const NetworkFee = ({
totalFee,
chain,
willRelay,
inline = false,
}: {
totalFee: string
chain: ChainInfo | undefined
willRelay: boolean
inline?: boolean
}) => {
const wallet = useWallet()

const isSocialLogin = isSocialLoginWallet(wallet?.label)

if (!isSocialLogin) {
return (
<Box
p={1}
sx={{
backgroundColor: lightPalette.secondary.background,
color: 'static.main',
width: 'fit-content',
borderRadius: '6px',
}}
>
<Typography variant="body1" className={classnames({ [css.sponsoredFee]: willRelay })}>
<Box className={classnames(css.networkFee, { [css.networkFeeInline]: inline })}>
<Typography className={classnames({ [css.sponsoredFee]: willRelay })}>
<b>
&asymp; {totalFee} {chain?.nativeCurrency.symbol}
</b>
Expand Down Expand Up @@ -156,6 +151,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
const router = useRouter()
const [gasPrice] = useGasPrice()
const [_, setPendingSafe] = usePendingSafe()
const [payMethod, setPayMethod] = useState(PayMethod.PayLater)
const [executionMethod, setExecutionMethod] = useState(ExecutionMethod.RELAY)
const [submitError, setSubmitError] = useState<string>()
const isCounterfactualEnabled = useHasFeature(FEATURES.COUNTERFACTUAL)
Expand Down Expand Up @@ -211,7 +207,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
const saltNonce = await getAvailableSaltNonce(provider, { ...props, saltNonce: '0' })
const safeAddress = await computeNewSafeAddress(provider, { ...props, saltNonce })

if (isCounterfactual) {
if (isCounterfactual && payMethod === PayMethod.PayLater) {
createCounterfactualSafe(chain, safeAddress, saltNonce, data, dispatch, props, router)
return
}
Expand Down Expand Up @@ -239,6 +235,39 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
<SafeSetupOverview name={data.name} owners={data.owners} threshold={data.threshold} />
</Box>

{isCounterfactual && (
<>
<Divider />
<Box className={layoutCss.row}>
<PayNowPayLater totalFee={totalFee} canRelay={canRelay} payMethod={payMethod} setPayMethod={setPayMethod} />

{canRelay && !isSocialLogin && payMethod === PayMethod.PayNow && (
<Grid container spacing={3} pt={2}>
<ReviewRow
value={
<ExecutionMethodSelector
executionMethod={executionMethod}
setExecutionMethod={setExecutionMethod}
relays={minRelays}
/>
}
/>
</Grid>
)}

{payMethod === PayMethod.PayNow && (
<Grid item>
<Typography mt={2}>
You will have to confirm a transaction and pay an estimated fee of{' '}
<NetworkFee totalFee={totalFee} willRelay={willRelay} chain={chain} inline /> with your connected
wallet
</Typography>
</Grid>
)}
</Box>
</>
)}

{!isCounterfactual && (
<>
<Divider />
Expand Down
13 changes: 13 additions & 0 deletions src/components/new-safe/create/steps/ReviewStep/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,16 @@
.errorMessage {
margin-top: 0;
}

.networkFee {
padding: var(--space-1);
background-color: var(--color-secondary-background);
color: var(--color-static-main);
width: fit-content;
border-radius: 6px;
}

.networkFeeInline {
padding: 2px 4px;
display: inline-flex;
}
Loading

0 comments on commit 7842373

Please sign in to comment.