Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Your storage modal #2941

Merged
merged 17 commits into from
Oct 5, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function DeployToWorld({
analytics.track('Publish to World step', { step: DeployToWorldView.FORM })
onRecord()
}
}, [ensList, onRecord, analytics])
}, [ensList, externalNames, onRecord, analytics])

useEffect(() => {
if (view === DeployToWorldView.FORM && loading && error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.total {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}

.total .mbs {
font-size: 2rem;
font-weight: bold;
}

.asset {
margin: 4rem 0;
display: flex;
justify-content: space-between;
align-items: center;
}

.asset .texts {
display: flex;
flex-direction: column;
}

.asset .texts .subtitle {
color: var(--secondary-text);
}

.asset .texts .amount {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
align-items: center;
}

.separator {
height: 1px;
background-color: var(--text);
width: 100%;
}

.proposal {
text-align: center;
}

.proposal .icon {
position: relative;
top: 2px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import * as React from 'react'
import { Button, ModalContent, ModalNavigation } from 'decentraland-ui'
import Modal from 'decentraland-dapps/dist/containers/Modal'
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
import { fromBytesToMegabytes } from 'components/WorldListPage_WorldsForEnsOwnersFeature/utils'
import { config } from 'config'
import { InfoIcon } from 'components/InfoIcon'
import goodImg from './images/good.svg'
import { Props, State, WorldsYourStorageModalMetadata } from './WorldsYourStorageModal.types'
import { fetchAccountHoldings, getMbsFromAccountHoldings } from './utils'
import styles from './WorldsYourStorageModal.module.css'

const MARKETPLACE_WEB_URL = config.get('MARKETPLACE_WEB_URL')
const ACCOUNT_URL = config.get('ACCOUNT_URL')

export default class WorldsYourStorageModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props)

this.state = {
accountHoldings: null
}
}

async componentDidMount() {
const { metadata } = this.props

const { stats } = metadata as WorldsYourStorageModalMetadata

const accountHoldings = await fetchAccountHoldings(stats.wallet)

this.setState({ accountHoldings })
}

render() {
const { name, onClose, metadata } = this.props

const { stats } = metadata as WorldsYourStorageModalMetadata

const { accountHoldings } = this.state

const mbsFromAccountHoldings = accountHoldings ? getMbsFromAccountHoldings(accountHoldings) : null

return (
<Modal name={name} onClose={onClose}>
<ModalNavigation title={t('worlds_your_storage_modal.your_storage')} onClose={onClose} />
<ModalContent>
<div className={styles.total}>
<span>{t('worlds_your_storage_modal.total_available_storage')}</span>
<span className={styles.mbs}>
{fromBytesToMegabytes(Number(stats.maxAllowedSpace) - Number(stats.usedSpace)).toFixed(2)} Mb
</span>
</div>
<div className={styles.asset}>
<div className={styles.texts}>
<span>{t('worlds_your_storage_modal.mana')}</span>
<span className={styles.subtitle}>{t('worlds_your_storage_modal.mana_earn_storage')}</span>
{accountHoldings && mbsFromAccountHoldings && mbsFromAccountHoldings.manaMbs > 0 ? (
<span className={styles.amount}>
<img src={goodImg} alt="good"></img>
{t('worlds_your_storage_modal.mana_holdings', {
mbs: mbsFromAccountHoldings.manaMbs,
owned: Math.trunc(accountHoldings.ownedMana)
})}
</span>
) : null}
</div>
<div>
<Button as="a" href={ACCOUNT_URL} primary>
{t('worlds_your_storage_modal.mana_buy')}
</Button>
</div>
</div>
<div className={styles.separator} />
<div className={styles.asset}>
<div className={styles.texts}>
<span>{t('worlds_your_storage_modal.lands')}</span>
<span className={styles.subtitle}>{t('worlds_your_storage_modal.lands_earn_storage')}</span>
{accountHoldings && mbsFromAccountHoldings && mbsFromAccountHoldings.landMbs > 0 ? (
<span className={styles.amount}>
<img src={goodImg} alt="good"></img>
{t('worlds_your_storage_modal.lands_holdings', {
mbs: mbsFromAccountHoldings.landMbs,
owned: accountHoldings.ownedLands
})}
</span>
) : null}
</div>
<div>
<Button as="a" href={MARKETPLACE_WEB_URL + '/lands'} primary>
{t('worlds_your_storage_modal.lands_buy')}
</Button>
</div>
</div>
<div className={styles.separator} />
<div className={styles.asset}>
<div className={styles.texts}>
<span>{t('worlds_your_storage_modal.names')}</span>
<span className={styles.subtitle}>{t('worlds_your_storage_modal.lands_earn_storage')}</span>
{accountHoldings && mbsFromAccountHoldings && mbsFromAccountHoldings.nameMbs > 0 ? (
<span className={styles.amount}>
<img src={goodImg} alt="good"></img>
{t('worlds_your_storage_modal.names_holdings', {
mbs: mbsFromAccountHoldings.nameMbs,
owned: accountHoldings.ownedNames
})}
</span>
) : null}
</div>
<div>
<Button as="a" href="/claim-name" primary>
{t('worlds_your_storage_modal.names_buy')}
</Button>
</div>
</div>
<div className={styles.proposal}>
<InfoIcon className={styles.icon} /> <span>{t('worlds_your_storage_modal.proposal')}</span>{' '}
<a href="https://governance.decentraland.org/proposal/?id=c3216070-e822-11ed-b8f1-75dbe089d333">
{t('worlds_your_storage_modal.learn_more')}
</a>
</div>
</ModalContent>
</Modal>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Dispatch } from 'redux'
import { ModalProps } from 'decentraland-dapps/dist/providers/ModalProvider/ModalProvider.types'
import { WorldsWalletStats } from 'lib/api/worlds'
import { AccountHoldings } from './utils'

export type Props = ModalProps & {
metadata: WorldsYourStorageModalMetadata
}

export type State = {
accountHoldings: AccountHoldings | null
}

export type WorldsYourStorageModalMetadata = {
stats: WorldsWalletStats
}

export type OwnProps = Pick<Props, 'metadata'>
export type MapDispatch = Dispatch
4 changes: 4 additions & 0 deletions src/components/Modals/WorldsYourStorageModal/images/good.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/Modals/WorldsYourStorageModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WorldsYourStorageModal from './WorldsYourStorageModal'

export default WorldsYourStorageModal
111 changes: 111 additions & 0 deletions src/components/Modals/WorldsYourStorageModal/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { AccountHoldings, fetchAccountHoldings, getMbsFromAccountHoldings } from './utils'

describe('when fetching the account holdings', () => {
let account: string

beforeEach(() => {
account = '0x123'
})

describe('when the request fails', () => {
beforeEach(() => {
jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('some error'))
})

it('should return null', async () => {
expect(await fetchAccountHoldings(account)).toBeNull()
})
})

describe('when the request does not fail', () => {
describe('when the response is not ok', () => {
beforeEach(() => {
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: false
} as Response)
})

it('should return null', async () => {
expect(await fetchAccountHoldings(account)).toBeNull()
})
})

describe('when the response is ok', () => {
let accountHoldings: AccountHoldings

beforeEach(() => {
accountHoldings = {} as AccountHoldings

jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: accountHoldings
})
} as Response)
})

it('should return the account holdings', async () => {
expect(await fetchAccountHoldings(account)).toEqual(accountHoldings)
})
})
})
})

describe('when gettings mbs from account holdings', () => {
let accountHoldings: AccountHoldings

describe('when account holdings has 10000 MANA, 100 LANDs and 100 NAMEs', () => {
beforeEach(() => {
accountHoldings = {
ownedMana: 10000,
ownedLands: 100,
ownedNames: 100
} as AccountHoldings
})

it('should return an object with 500 manaMbs, 10000 landMbs and 10000 nameMbs', () => {
expect(getMbsFromAccountHoldings(accountHoldings)).toEqual({
manaMbs: 500,
landMbs: 10000,
nameMbs: 10000
})
})
})

describe('when account holdings has 0 MANA, 0 LANDs and 0 NAMEs', () => {
beforeEach(() => {
accountHoldings = {
ownedMana: 0,
ownedLands: 0,
ownedNames: 0
} as AccountHoldings
})

it('should return an object with 0 manaMbs, 0 landMbs and 0 nameMbs', () => {
expect(getMbsFromAccountHoldings(accountHoldings)).toEqual({
manaMbs: 0,
landMbs: 0,
nameMbs: 0
})
})
})

describe('when account holdings has 1999 MANA, 0 LANDs and 0 NAMEs', () => {
beforeEach(() => {
accountHoldings = {
ownedMana: 1999,
ownedLands: 0,
ownedNames: 0
} as AccountHoldings
})

it('should return an object with 0 manaMbs, 0 landMbs and 0 nameMbs', () => {
expect(getMbsFromAccountHoldings(accountHoldings)).toEqual({
manaMbs: 0,
landMbs: 0,
nameMbs: 0
})
})
})
})
43 changes: 43 additions & 0 deletions src/components/Modals/WorldsYourStorageModal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { config } from 'config'

const dclNameStatsUrl = config.get('DCL_NAME_STATS_URL')

export type AccountHoldings = {
owner: string
ownedLands: number
ownedNames: number
ownedMana: number
spaceAllowance: number
}

export const fetchAccountHoldings = async (account: string) => {
let response: Response

try {
response = await fetch(`${dclNameStatsUrl}/account-holdings/${account}`, {
method: 'POST'
})
} catch (e) {
return null
}

if (!response.ok) {
return null
}

const { data } = await response.json()

return data as AccountHoldings
}

export const getMbsFromAccountHoldings = (accountHoldings: AccountHoldings) => {
const manaMbs = Math.trunc(accountHoldings.ownedMana / 2000) * 100
const landMbs = accountHoldings.ownedLands * 100
const nameMbs = accountHoldings.ownedNames * 100

return {
manaMbs,
landMbs,
nameMbs
}
}
1 change: 1 addition & 0 deletions src/components/Modals/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export { default as SeeInWorldModal } from './SeeInWorldModal'
export { default as SceneCreationModal } from './SceneCreationModal'
export { default as EditVideoModal } from './EditVideoModal'
export { default as EmotesV2AnnouncementModal } from './EmotesV2AnnouncementModal'
export { default as WorldsYourStorageModal } from './WorldsYourStorageModal'
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getProjects } from 'modules/ui/dashboard/selectors'
import { getConnectedWalletStats, getLoading as getLoadingWorlds } from 'modules/worlds/selectors'
import { MapStateProps, MapDispatchProps, MapDispatch } from './WorldListPage.types'
import WorldListPage from './WorldListPage'
import { openModal } from 'modules/modal/actions'

const mapState = (state: RootState): MapStateProps => ({
ensList: getENSByWallet(state),
Expand All @@ -37,7 +38,8 @@ const mapState = (state: RootState): MapStateProps => ({
})

const mapDispatch = (dispatch: MapDispatch): MapDispatchProps => ({
onNavigate: path => dispatch(push(path))
onNavigate: path => dispatch(push(path)),
onOpenModal: metadata => dispatch(openModal('WorldsYourStorageModal', metadata))
})

export default connect(mapState, mapDispatch)(WorldListPage)
Loading
Loading