Skip to content

Commit

Permalink
Merge 30079cc into c9b0c6a
Browse files Browse the repository at this point in the history
  • Loading branch information
meelrossi committed Jan 26, 2024
2 parents c9b0c6a + 30079cc commit 8d6c8bb
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { connect } from 'react-redux'
import { SET_ENS_ADDRESS_REQUEST, setENSAddressRequest } from 'modules/ens/actions'
import {
SET_ENS_ADDRESS_REQUEST,
SET_ENS_RESOLVER_REQUEST,
clearENSErrors,
setENSAddressRequest,
setENSResolverRequest
} from 'modules/ens/actions'
import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors'
import { getError, getLoading, isWaitingTxSetAddress } from 'modules/ens/selectors'
import { getENSBySubdomain, getError, getLoading, isWaitingTxSetAddress, isWaitingTxSetResolver } from 'modules/ens/selectors'
import { RootState } from 'modules/common/types'
import { MapDispatch, MapDispatchProps, OwnProps } from './ENSMapAddressModal.types'
import EnsMapAddressModal from './ENSMapAddressModal'

const mapState = (state: RootState) => {
const mapState = (state: RootState, ownProps: OwnProps) => {
const error = getError(state)
return {
isLoading: isLoadingType(getLoading(state), SET_ENS_ADDRESS_REQUEST) || isWaitingTxSetAddress(state),
error: error ? error.message : null
isLoadingSetResolver: isLoadingType(getLoading(state), SET_ENS_RESOLVER_REQUEST),
isWaitingTxSetResolver: isWaitingTxSetResolver(state),
error: error ? error.message : null,
ens: getENSBySubdomain(state, ownProps.metadata.ens.subdomain)
}
}

const mapDispatch = (dispatch: MapDispatch, ownProps: OwnProps): MapDispatchProps => ({
onSave: ((address: string) => dispatch(setENSAddressRequest(ownProps.metadata.ens, address))) as any
onSave: ((address: string) => dispatch(setENSAddressRequest(ownProps.metadata.ens, address))) as any,
onUnmount: () => dispatch(clearENSErrors()),
onSetENSResolver: ens => dispatch(setENSResolverRequest(ens))
})

export default connect(mapState, mapDispatch)(EnsMapAddressModal)
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,26 @@
.ethereum img {
width: 24px;
}

.setResolverInfo {
font-size: 16px;
font-weight: 400;
line-height: 24px;
}

.setResolverActions {
display: flex;
gap: 20px;
}

.setResolverStatus {
display: flex;
align-items: center;
flex: 1;
gap: 10px;
font-size: 13px;
}

.setResolverStatus .icon {
color: var(--primary);
}
146 changes: 104 additions & 42 deletions src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,136 @@ import { ENS } from 'modules/ens/types'
import { renderWithProviders } from 'specs/utils'
import { Props } from './ENSMapAddressModal.types'
import EnsMapAddressModal from './ENSMapAddressModal'
import { ENS_RESOLVER_ADDRESS } from 'modules/common/contracts'

function renderENSMapAddressModal(props: Partial<Props> = {}) {
return renderWithProviders(
<EnsMapAddressModal isLoading={false} error={null} onSave={jest.fn()} onClose={jest.fn()} metadata={{ ens: {} }} {...props} />
<EnsMapAddressModal
isLoading={false}
isLoadingSetResolver={false}
ens={{ resolver: '' } as ENS}
isWaitingTxSetResolver={false}
error={null}
onSave={jest.fn()}
onUnmount={jest.fn()}
onClose={jest.fn()}
onSetENSResolver={jest.fn()}
{...props}
/>
)
}

let screen: RenderResult

describe('when clicking close button', () => {
it('should call onClose callback', () => {
const onClose = jest.fn()
renderENSMapAddressModal({ onClose })
const closeBtn = document.querySelector('.dcl.close')
if (closeBtn) {
userEvent.click(closeBtn)
}
expect(onClose).toHaveBeenCalled()
describe('when the resolver is defined for the ens', () => {
let ens: ENS
beforeEach(() => {
ens = {
name: 'test',
subdomain: 'test.dcl.eth',
resolver: ENS_RESOLVER_ADDRESS
} as ENS
})
})

describe('when address is not defined', () => {
it('should disable save button', () => {
const screen = renderENSMapAddressModal()
expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled()
describe('when clicking close button', () => {
it('should call onClose callback', () => {
const onClose = jest.fn()
renderENSMapAddressModal({ onClose, ens })
const closeBtn = document.querySelector('.dcl.close')
if (closeBtn) {
userEvent.click(closeBtn)
}
expect(onClose).toHaveBeenCalled()
})
})
})

describe('when address is defined', () => {
it('should call onSave when pressing save button', () => {
const onSave = jest.fn()
const screen = renderENSMapAddressModal({ onSave })
const addressInput = screen.getByLabelText(t('ens_map_address_modal.address.label'))
userEvent.type(addressInput, '0xtestaddress')
const saveButton = screen.getByRole('button', { name: t('ens_map_address_modal.save') })
userEvent.click(saveButton)
expect(onSave).toHaveBeenCalledWith('0xtestaddress')
describe('when address is not defined', () => {
it('should disable save button', () => {
const screen = renderENSMapAddressModal({ ens })
expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled()
})
})
})

describe('when linking address is loading', () => {
beforeEach(() => {
screen = renderENSMapAddressModal({ isLoading: true })
})
it('should disable save button', () => {
expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled()
describe('when address is defined', () => {
it('should call onSave when pressing save button', () => {
const onSave = jest.fn()
const screen = renderENSMapAddressModal({ onSave, ens })
const addressInput = screen.getByLabelText(t('ens_map_address_modal.address.label'))
userEvent.type(addressInput, '0xtestaddress')
const saveButton = screen.getByRole('button', { name: t('ens_map_address_modal.save') })
userEvent.click(saveButton)
expect(onSave).toHaveBeenCalledWith('0xtestaddress')
})
})

it('should disable address input', () => {
expect(screen.getByLabelText(t('ens_map_address_modal.address.label'))).toBeDisabled()
describe('when linking address is loading', () => {
beforeEach(() => {
screen = renderENSMapAddressModal({ isLoading: true, ens })
})
it('should disable save button', () => {
expect(screen.getByRole('button', { name: t('ens_map_address_modal.save') })).toBeDisabled()
})

it('should disable address input', () => {
expect(screen.getByLabelText(t('ens_map_address_modal.address.label'))).toBeDisabled()
})

it('should not show close icon', () => {
const closeBtn = document.querySelector('.dcl.close')
expect(closeBtn).toBe(null)
})
})

it('should not show close icon', () => {
const closeBtn = document.querySelector('.dcl.close')
expect(closeBtn).toBe(null)
describe('when editing an address', () => {
beforeEach(() => {
ens = {
...ens,
ensAddressRecord: '0xtest123'
} as ENS
screen = renderENSMapAddressModal({ isLoading: true, ens })
})

it('should set the existing address as the input value', () => {
expect(screen.getByLabelText(t('ens_map_address_modal.address.label'))).toHaveValue(ens.ensAddressRecord)
})
})
})

describe('when editing an address', () => {
describe('when there is no resolver for the ens', () => {
let ens: ENS
beforeEach(() => {
ens = {
ensAddressRecord: '0xtest123'
name: 'test',
subdomain: 'test.dcl.eth',
resolver: ''
} as ENS
screen = renderENSMapAddressModal({ isLoading: true, metadata: { ens } })
})

it('should set the existing address as the input value', () => {
expect(screen.getByLabelText(t('ens_map_address_modal.address.label'))).toHaveValue(ens.ensAddressRecord)
it('should render set resolver view', () => {
const screen = renderENSMapAddressModal({ ens })
expect(screen.getByText(t('ens_map_address_modal.set_resolver.title'))).toBeInTheDocument()
})

it('should call onSetENSResolver callback when clicking the action button', () => {
const onSetENSResolverFn = jest.fn()
const screen = renderENSMapAddressModal({ ens, onSetENSResolver: onSetENSResolverFn })
const actionBtn = screen.getByRole('button', { name: t('ens_map_address_modal.set_resolver.action') })
userEvent.click(actionBtn)
expect(onSetENSResolverFn).toHaveBeenCalledWith(ens)
})

it('should show error message when an error ocurred', () => {
const screen = renderENSMapAddressModal({ ens, error: 'Some error ocurr' })
expect(screen.getByText(t('ens_map_address_modal.set_resolver.error'))).toBeInTheDocument()
})

it('should show confirm transaction message when set resolver is loading', () => {
const screen = renderENSMapAddressModal({ ens, isLoadingSetResolver: true })
expect(screen.getByText(t('ens_map_address_modal.confirm_transaction'))).toBeInTheDocument()
})

it('should show processing message when set resolver tx is loading', () => {
const screen = renderENSMapAddressModal({ ens, isWaitingTxSetResolver: true })
expect(screen.getByText(t('ens_map_address_modal.processing'))).toBeInTheDocument()
})
})
72 changes: 64 additions & 8 deletions src/components/Modals/ENSMapAddressModal/ENSMapAddressModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { useCallback, useEffect, useState } from 'react'
import { Button, Close, Field, Message } from 'decentraland-ui'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Close, Field, Icon, Loader, Message } from 'decentraland-ui'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import Modal from 'decentraland-dapps/dist/containers/Modal'
import { ENS_RESOLVER_ADDRESS } from 'modules/common/contracts'
import { isResolverEmpty } from 'modules/ens/utils'
import ethereumImg from '../../../icons/ethereum.svg'
import { Props } from './ENSMapAddressModal.types'
import styles from './ENSMapAddressModal.module.css'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'

export default function EnsMapAddressModal(props: Props) {
const { isLoading, error, onClose, onSave } = props
const [address, setAddress] = useState<string>(props.metadata.ens.ensAddressRecord)
const { isLoading, error, ens, isLoadingSetResolver, isWaitingTxSetResolver, onUnmount, onClose, onSave, onSetENSResolver } = props
const [address, setAddress] = useState<string>(ens.ensAddressRecord || '')

const hasResolver = !isResolverEmpty(ens) && ens.resolver.toLowerCase() === ENS_RESOLVER_ADDRESS.toLowerCase()

const handleSave = useCallback(() => {
onSave(address)
Expand All @@ -21,12 +25,64 @@ export default function EnsMapAddressModal(props: Props) {
[setAddress]
)

const handleSetENSResolver = useCallback(() => {
onSetENSResolver(ens)
}, [ens, onSetENSResolver])

useEffect(() => {
document.getElementById('address-input')?.focus()
}, [])
return () => {
onUnmount()
}
}, [onUnmount])

const setResolverMessage = useMemo(() => {
if (isLoadingSetResolver) {
return (
<span className={styles.setResolverStatus}>
<Loader inline active size="tiny" />
{t('ens_map_address_modal.confirm_transaction')}
</span>
)
} else if (isWaitingTxSetResolver) {
return (
<span className={styles.setResolverStatus}>
<Loader inline active size="tiny" />
{t('ens_map_address_modal.processing')}
</span>
)
} else if (error) {
return (
<span className={styles.setResolverStatus}>
<Icon name="close" className={styles.icon} />
{t('ens_map_address_modal.set_resolver.error')}
</span>
)
}
return null
}, [isLoadingSetResolver, isWaitingTxSetResolver, error])

const disableActions = isLoading || isLoadingSetResolver || isWaitingTxSetResolver

if (!hasResolver) {
return (
<Modal closeIcon={disableActions ? undefined : <Close />} onClose={disableActions ? undefined : onClose} size="tiny">
<div className={styles.main}>
<h1>{t('ens_map_address_modal.set_resolver.title')}</h1>
<span className={styles.setResolverInfo}>{t('ens_map_address_modal.set_resolver.info', { br: () => <br /> })}</span>
<div className={styles.setResolverActions}>
<Button primary onClick={handleSetENSResolver} disabled={disableActions}>
{t('ens_map_address_modal.set_resolver.action')}
</Button>
{setResolverMessage}
</div>
</div>
</Modal>
)
}

return (
<Modal closeIcon={isLoading ? undefined : <Close />} onClose={isLoading ? undefined : onClose} size="tiny">
<Modal closeIcon={disableActions ? undefined : <Close />} onClose={disableActions ? undefined : onClose} size="tiny">
<div className={styles.main}>
<div className={styles.info}>
<h1 className={styles.title}>{t('ens_map_address_modal.title')}</h1>
Expand All @@ -53,7 +109,7 @@ export default function EnsMapAddressModal(props: Props) {
{error && <Message error size="tiny" visible content={error} header={t('ens_map_address_modal.error_title')} />}
<div className={styles.actions}>
<Button>{t('ens_map_address_modal.learn_more')}</Button>
<Button primary onClick={handleSave} disabled={!address || isLoading} loading={isLoading}>
<Button primary onClick={handleSave} disabled={!address || disableActions} loading={isLoading}>
{t('ens_map_address_modal.save')}
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { ModalProps } from 'decentraland-ui'
import { Dispatch } from 'redux'
import { SetENSAddressRequestAction } from 'modules/ens/actions'
import {
ClearENSErrorsAction,
SetENSAddressRequestAction,
SetENSResolverRequestAction,
clearENSErrors,
setENSResolverRequest
} from 'modules/ens/actions'
import { ENS } from 'modules/ens/types'

export type Props = ModalProps & {
error: string | null
isLoading: boolean
isLoadingSetResolver: boolean
isWaitingTxSetResolver: boolean
ens: ENS
onSave: (address: string) => void
onUnmount: typeof clearENSErrors
onSetENSResolver: typeof setENSResolverRequest
}

export type MapStateProps = Pick<Props, 'error' | 'isLoading'>
export type MapDispatchProps = Pick<Props, 'onSave'>
export type MapDispatch = Dispatch<SetENSAddressRequestAction>
export type MapStateProps = Pick<Props, 'error' | 'isLoading' | 'isLoadingSetResolver' | 'isWaitingTxSetResolver' | 'ens'>
export type MapDispatchProps = Pick<Props, 'onSave' | 'onUnmount' | 'onSetENSResolver'>
export type MapDispatch = Dispatch<SetENSAddressRequestAction | ClearENSErrorsAction | SetENSResolverRequestAction>
export type OwnProps = Omit<Props, keyof MapDispatchProps>
6 changes: 6 additions & 0 deletions src/modules/analytics/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { LandTile, Rental } from 'modules/land/types'
import { getLandTiles, getRentals } from 'modules/land/selectors'
import { LOGIN_SUCCESS, LoginSuccessAction } from 'modules/identity/actions'
import { PublishThirdPartyItemsSuccessAction, PUBLISH_THIRD_PARTY_ITEMS_SUCCESS } from 'modules/thirdParty/actions'
import { SET_ENS_ADDRESS_SUCCESS, SetENSAddressSuccessAction } from 'modules/ens/actions'

const baseAnalyticsSaga = createAnalyticsSaga()

Expand All @@ -70,6 +71,7 @@ function* builderAnalyticsSaga() {
yield takeLatest(DELETE_ASSET_PACK_FAILURE, handleDeleteAssetPackFailure)
yield takeLatest(PUBLISH_THIRD_PARTY_ITEMS_SUCCESS, handlePublishTPItemSuccess)
yield takeLatest(DEPLOY_TO_WORLD_SUCCESS, handleDeployToWorldSuccess)
yield takeLatest(SET_ENS_ADDRESS_SUCCESS, handleSetENSAddressSuccess)
}

export function* analyticsSaga() {
Expand Down Expand Up @@ -231,3 +233,7 @@ function* handleDeployToWorldSuccess(action: DeployToWorldSuccessAction) {

yield call(track, '[Success] Deploy to World', { project_id: project.id, eth_address: ethAddress, subdomain: deployment.world })
}

function* handleSetENSAddressSuccess(action: SetENSAddressSuccessAction) {
yield call(track, 'Map Address to Name', { name: action.payload.ens.name, address: action.payload.address })
}
Loading

0 comments on commit 8d6c8bb

Please sign in to comment.