feat: update postage stamp creation screen (#641)

* style: UI changes for postage stamp

* feat: New postage stamp standard page

---------

Co-authored-by: Seres Roland <seresroland@Seres-MBP.home>
This commit is contained in:
rolandlor
2023-12-04 22:01:51 +01:00
committed by GitHub
parent 20a051b658
commit 4f9abc614e
12 changed files with 392 additions and 87 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ export function AccountStamps(): ReactElement {
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
function navigateToNewStamp() {
navigate(ROUTES.ACCOUNT_STAMPS_NEW)
navigate(ROUTES.ACCOUNT_STAMPS_NEW_STANDARD)
}
return (
+2 -2
View File
@@ -18,7 +18,7 @@ import { detectIndexHtml, getAssetNameFromFiles, packageFile } from '../../utils
import { persistIdentity, updateFeed } from '../../utils/identity'
import { HISTORY_KEYS, putHistory } from '../../utils/local-storage'
import { FeedPasswordDialog } from '../feeds/FeedPasswordDialog'
import { PostageStampCreation } from '../stamps/PostageStampCreation'
import { PostageStampAdvancedCreation } from '../stamps/PostageStampAdvancedCreation'
import { PostageStampSelector } from '../stamps/PostageStampSelector'
import { AssetPreview } from './AssetPreview'
import { StampPreview } from './StampPreview'
@@ -186,7 +186,7 @@ export function Upload(): ReactElement {
{hasAnyStamps && stampMode === 'SELECT' ? (
<PostageStampSelector onSelect={stamp => setStamp(stamp)} defaultValue={stamp?.batchID} />
) : (
<PostageStampCreation onFinished={() => setStampMode('SELECT')} />
<PostageStampAdvancedCreation onFinished={() => setStampMode('SELECT')} />
)}
</Box>
<Box mb={4}>
@@ -2,7 +2,7 @@ import { ReactElement } from 'react'
import { useNavigate } from 'react-router'
import { HistoryHeader } from '../../components/HistoryHeader'
import { ROUTES } from '../../routes'
import { PostageStampCreation } from './PostageStampCreation'
import { PostageStampAdvancedCreation } from './PostageStampAdvancedCreation'
export function CreatePostageStampPage(): ReactElement {
const navigate = useNavigate()
@@ -13,8 +13,8 @@ export function CreatePostageStampPage(): ReactElement {
return (
<div>
<HistoryHeader>Buy new postage stamp</HistoryHeader>
<PostageStampCreation onFinished={onFinished} />
<HistoryHeader>Buy new postage stamp batch</HistoryHeader>
<PostageStampAdvancedCreation onFinished={onFinished} />
</div>
)
}
@@ -0,0 +1,20 @@
import { ReactElement } from 'react'
import { useNavigate } from 'react-router'
import { HistoryHeader } from '../../components/HistoryHeader'
import { ROUTES } from '../../routes'
import { PostageStampStandardCreation } from './PostageStampStandardCreation'
export function CreatePostageStampBasicPage(): ReactElement {
const navigate = useNavigate()
function onFinished() {
navigate(ROUTES.ACCOUNT_STAMPS)
}
return (
<div>
<HistoryHeader>Buy new postage stamp batch</HistoryHeader>
<PostageStampStandardCreation onFinished={onFinished} />
</div>
)
}
@@ -1,5 +1,6 @@
import { PostageBatchOptions } from '@ethersphere/bee-js'
import { Box, Grid, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import BigNumber from 'bignumber.js'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
@@ -18,12 +19,32 @@ import {
waitUntilStampExists,
} from '../../utils'
import { getHumanReadableFileSize } from '../../utils/file'
import { Link } from 'react-router-dom'
import { ROUTES } from '../../routes'
interface Props {
onFinished: () => void
}
export function PostageStampCreation({ onFinished }: Props): ReactElement {
const useStyles = makeStyles((theme: Theme) =>
createStyles({
link: {
color: '#dd7700',
textDecoration: 'underline',
'&:hover': {
textDecoration: 'none',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
textDecoration: 'none',
},
},
},
}),
)
export function PostageStampAdvancedCreation({ onFinished }: Props): ReactElement {
const classes = useStyles()
const { chainState } = useContext(BeeContext)
const { refresh } = useContext(StampsContext)
const { beeDebugApi } = useContext(SettingsContext)
@@ -153,22 +174,25 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
return (
<>
<Box mb={4}>
<Typography>
To upload data to Swarm network, you will need to purchase a postage stamp. If you&apos;re not familiar with
this, please read{' '}
<a
href="https://medium.com/ethereum-swarm/how-to-upload-data-to-the-swarm-network-c0766c3ae381"
target="_blank"
rel="noreferrer"
>
this guide
</a>
.
</Typography>
<Box mb={1}>
<Typography variant="h2">Batch name</Typography>
</Box>
<Box mb={2}>
<SwarmTextInput name="depth" label="Depth" onChange={event => validateDepthInput(event.target.value)} />
<SwarmTextInput name="label" label="Label" optional onChange={event => setLabelInput(event.target.value)} />
</Box>
<Box mb={2}>
<SwarmSelect
label="Immutable"
defaultValue="No"
onChange={event => setImmutable(event.target.value === 'Yes')}
options={[
{ value: 'Yes', label: 'Yes' },
{ value: 'No', label: 'No' },
]}
/>
</Box>
<Box mb={2}>
<SwarmTextInput name="depth" label="Batch depth" onChange={event => validateDepthInput(event.target.value)} />
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Corresponding file size</Typography>
@@ -185,38 +209,14 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
<Typography>{!amountError && amountInput ? getTtl(Number.parseInt(amountInput, 10)) : '-'}</Typography>
</Grid>
</Box>
<Box display="flex" justifyContent={'right'} mt={0.5}>
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}>
Current price of 24000 per block
</Typography>
</Box>
{amountError && <Typography>{amountError}</Typography>}
</Box>
<Box mb={2}>
<SwarmTextInput name="label" label="Label" optional onChange={event => setLabelInput(event.target.value)} />
</Box>
<Box mb={2}>
<SwarmSelect
label="Immutable"
defaultValue="No"
onChange={event => setImmutable(event.target.value === 'Yes')}
options={[
{ value: 'Yes', label: 'Yes' },
{ value: 'No', label: 'No' },
]}
/>
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between">
{immutable && (
<Typography>
Once an immutable stamp is maxed out, it disallows further content uploads, thereby safeguarding your
previously uploaded content from unintentional overwriting.
</Typography>
)}
{!immutable && (
<Typography>
When a mutable stamp reaches full capacity, it still permits new content uploads. However, this comes
with the caveat of overwriting previously uploaded content associated with the same stamp.
</Typography>
)}
</Grid>
</Box>
</Box>
<Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Indicative Price</Typography>
@@ -227,14 +227,24 @@ export function PostageStampCreation({ onFinished }: Props): ReactElement {
</Typography>
</Grid>
</Box>
<SwarmButton
disabled={submitting || Boolean(depthError) || Boolean(amountError) || !depthInput || !amountInput}
onClick={submit}
iconType={Check}
loading={submitting}
>
Buy New Stamp
</SwarmButton>
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<SwarmButton
disabled={submitting || Boolean(depthError) || Boolean(amountError) || !depthInput || !amountInput}
onClick={submit}
iconType={Check}
loading={submitting}
>
Buy New Stamp
</SwarmButton>
</Grid>
<Grid item>
<Link to={ROUTES.ACCOUNT_STAMPS_NEW_STANDARD} className={classes.link}>
Standard mode
</Link>
</Grid>
</Grid>
</>
)
}
@@ -0,0 +1,230 @@
import { PostageBatchOptions } from '@ethersphere/bee-js'
import { Utils } from '@ethersphere/bee-js'
import { Box, Button, Grid, Slider, Typography } from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { useSnackbar } from 'notistack'
import { ReactElement, useContext, useState } from 'react'
import Check from 'remixicon-react/CheckLineIcon'
import { SwarmButton } from '../../components/SwarmButton'
import { SwarmTextInput } from '../../components/SwarmTextInput'
import { Context as SettingsContext } from '../../providers/Settings'
import { Context as StampsContext } from '../../providers/Stamps'
import { Link } from 'react-router-dom'
import { ROUTES } from '../../routes'
import { calculateStampPrice, convertAmountToSeconds, secondsToTimeString, waitUntilStampExists } from '../../utils'
interface Props {
onFinished: () => void
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
link: {
color: '#dd7700',
textDecoration: 'underline',
'&:hover': {
textDecoration: 'none',
// https://github.com/mui-org/material-ui/issues/22543
'@media (hover: none)': {
textDecoration: 'none',
},
},
},
buttonSelected: {
color: 'white',
backgroundColor: theme.palette.primary.main,
},
}),
)
const marks = [
{ value: 1, label: '1 day' },
{ value: 365, label: '365 days' },
]
export function PostageStampStandardCreation({ onFinished }: Props): ReactElement {
const getDepthForCapacity = Utils.getDepthForCapacity
const getAmountForTtl = Utils.getAmountForTtl
const classes = useStyles()
const { refresh } = useContext(StampsContext)
const { beeDebugApi } = useContext(SettingsContext)
const [depthInput, setDepthInput] = useState<number>(getDepthForCapacity(4))
const [amountInput, setAmountInput] = useState<number>(Number.parseInt(getAmountForTtl(30)))
const [labelInput, setLabelInput] = useState('')
const [submitting, setSubmitting] = useState(false)
const [buttonValue, setButtonValue] = useState(4)
function sliderValueChange(event: any, newValue: any) {
const amountValue = Number.parseInt(getAmountForTtl(newValue))
setAmountInput(amountValue)
}
const { enqueueSnackbar } = useSnackbar()
function getTtl(amount: number): string {
const pricePerBlock = 24000
return `${secondsToTimeString(
convertAmountToSeconds(amount, pricePerBlock),
)} (with price of ${pricePerBlock.toFixed(0)} per block)`
}
function getPrice(depth: number, amount: bigint): string {
const price = calculateStampPrice(depth, amount)
return `${price.toSignificantDigits()} xBZZ`
}
async function submit() {
try {
// This is really just a typeguard, the validation pretty much guarantees these will have the right values
if (!depthInput || !amountInput) {
return
}
if (!beeDebugApi) {
return
}
setSubmitting(true)
const amount = BigInt(amountInput)
const depth = depthInput
const options: PostageBatchOptions = {
waitForUsable: false,
label: labelInput || undefined,
immutableFlag: false,
}
const batchId = await beeDebugApi.createPostageBatch(amount.toString(), depth, options)
await waitUntilStampExists(batchId, beeDebugApi)
await refresh()
onFinished()
} catch (e) {
console.error(e) // eslint-disable-line
enqueueSnackbar(`Error: ${(e as Error).message}`, { variant: 'error' })
}
setSubmitting(false)
}
function handleBatchSize(event: any) {
let value = event.target.innerText
value = Number(value.substring(0, value.length - 3))
setButtonValue(value)
const capacity = getDepthForCapacity(value)
setDepthInput(capacity)
}
return (
<>
<Box mb={4}>
<Typography>
A postage stamp batch containes postage stamps that will give you the right to upload data to the Swarm
network. If you&apos;re not familiar with this, please read&nbsp;
<a
href="https://medium.com/ethereum-swarm/how-to-upload-data-to-the-swarm-network-c0766c3ae381"
target="_blank"
rel="noreferrer"
>
this guide
</a>
.
</Typography>
</Box>
<Box mb={1}>
<Typography variant="h2">Batch name</Typography>
</Box>
<Box mb={2}>
<SwarmTextInput name="depth" label="Label" onChange={e => setLabelInput(e.target.value)} />
</Box>
<Box mb={1}>
<Typography variant="h2">Batch size</Typography>
</Box>
<Box mb={2}>
<Grid container justifyContent="space-between" spacing={2}>
<Grid item xs={4}>
<Button
variant="contained"
fullWidth
onClick={handleBatchSize}
className={buttonValue === 4 ? classes.buttonSelected : ''}
>
4 GB
</Button>
</Grid>
<Grid item xs={4}>
<Button
variant="contained"
fullWidth
onClick={handleBatchSize}
className={buttonValue === 32 ? classes.buttonSelected : ''}
>
32 GB
</Button>
</Grid>
<Grid item xs={4}>
<Button
variant="contained"
fullWidth
onClick={handleBatchSize}
className={buttonValue === 256 ? classes.buttonSelected : ''}
>
256 GB
</Button>
</Grid>
</Grid>
</Box>
<Box mb={1}>
<Typography variant="h2">Data persistence</Typography>
</Box>
<Box mb={2}>
<Slider
aria-label="Volume"
min={1}
max={365}
step={1}
marks={marks}
valueLabelDisplay="auto"
defaultValue={30}
onChange={sliderValueChange}
/>
</Box>
<Box mb={2}>
<Box mt={0.25} sx={{ bgcolor: '#f6f6f6' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Corresponding TTL (Time to live)</Typography>
<Typography>{amountInput ? getTtl(amountInput) : '-'}</Typography>
</Grid>
</Box>
<Box display="flex" justifyContent={'right'} mt={0.5}>
<Typography style={{ fontSize: '10px', color: 'rgba(0, 0, 0, 0.26)' }}>
Current price of 24000 per block
</Typography>
</Box>
</Box>
<Box mb={4} sx={{ bgcolor: '#fcf2e8' }} p={2}>
<Grid container justifyContent="space-between">
<Typography>Indicative Price</Typography>
<Typography>{getPrice(depthInput, BigInt(amountInput))}</Typography>
</Grid>
</Box>
<Grid container justifyContent="space-between" alignItems="center">
<Grid item>
<SwarmButton
disabled={submitting || !depthInput || !amountInput}
onClick={submit}
iconType={Check}
loading={submitting}
>
Buy New Stamp
</SwarmButton>
</Grid>
<Grid item>
<Link to={ROUTES.ACCOUNT_STAMPS_NEW_ADVANCED} className={classes.link}>
Advanced mode
</Link>
</Grid>
</Grid>
</>
)
}
+1 -1
View File
@@ -44,7 +44,7 @@ export default function Stamp(): ReactElement {
if (status.all === CheckState.ERROR) return <TroubleshootConnectionCard />
function navigateToNewStamp() {
navigate(ROUTES.ACCOUNT_STAMPS_NEW)
navigate(ROUTES.ACCOUNT_STAMPS_NEW_STANDARD)
}
return (
+6 -3
View File
@@ -16,7 +16,7 @@ import GiftCards from './pages/gift-code'
import Info from './pages/info'
import LightModeRestart from './pages/restart/LightModeRestart'
import Settings from './pages/settings'
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampPage'
import { CreatePostageStampPage } from './pages/stamps/CreatePostageStampAdvancedPage'
import Status from './pages/status'
import TopUp from './pages/top-up'
import { BankCardTopUpIndex } from './pages/top-up/BankCardTopUpIndex'
@@ -25,6 +25,7 @@ import { GiftCardFund } from './pages/top-up/GiftCardFund'
import { GiftCardTopUpIndex } from './pages/top-up/GiftCardTopUpIndex'
import { Swap } from './pages/top-up/Swap'
import { Context as SettingsContext } from './providers/Settings'
import { CreatePostageStampBasicPage } from './pages/stamps/CreatePostageStampStandardPage'
export enum ROUTES {
INFO = '/',
@@ -46,7 +47,8 @@ export enum ROUTES {
ACCOUNT_WALLET = '/account/wallet',
ACCOUNT_CHEQUEBOOK = '/account/chequebook',
ACCOUNT_STAMPS = '/account/stamps',
ACCOUNT_STAMPS_NEW = '/account/stamps/new',
ACCOUNT_STAMPS_NEW_STANDARD = '/account/stamps/new',
ACCOUNT_STAMPS_NEW_ADVANCED = '/account/stamps/new/advanced',
ACCOUNT_FEEDS = '/account/feeds',
ACCOUNT_FEEDS_NEW = '/account/feeds/new',
ACCOUNT_FEEDS_UPDATE = '/account/feeds/update/:hash',
@@ -86,7 +88,8 @@ const BaseRouter = (): ReactElement => {
<Route path={ROUTES.ACCOUNT_WALLET} element={<AccountWallet />} />
<Route path={ROUTES.ACCOUNT_CHEQUEBOOK} element={<AccountChequebook />} />
<Route path={ROUTES.ACCOUNT_STAMPS} element={<AccountStamps />} />
<Route path={ROUTES.ACCOUNT_STAMPS_NEW} element={<CreatePostageStampPage />} />
<Route path={ROUTES.ACCOUNT_STAMPS_NEW_STANDARD} element={<CreatePostageStampBasicPage />} />
<Route path={ROUTES.ACCOUNT_STAMPS_NEW_ADVANCED} element={<CreatePostageStampPage />} />
<Route path={ROUTES.ACCOUNT_FEEDS} element={<AccountFeeds />} />
<Route path={ROUTES.ACCOUNT_FEEDS_NEW} element={<CreateNewFeed />} />
<Route path={ROUTES.ACCOUNT_FEEDS_UPDATE} element={<UpdateFeed />} />
+17
View File
@@ -159,6 +159,23 @@ const componentsOverrides = (theme: Theme) => ({
backgroundColor: 'transparent',
},
},
MuiSlider: {
root: {
'& .MuiSlider-valueLabel': {
top: '-27px',
'& span': {
height: '20px',
borderRadius: '0px',
transform: 'none',
'& span': {
display: 'flex',
alignItems: 'center',
transform: 'none',
},
},
},
},
},
})
const propsOverrides = {
+25
View File
@@ -0,0 +1,25 @@
export function getStampUsage(utilization: number, depth: number, bucketDepth: number): number {
return utilization / Math.pow(2, depth - bucketDepth)
}
export function getStampMaximumCapacityBytes(depth: number): number {
return 2 ** depth * 4096
}
export function getStampCostInPlur(depth: number, amount: number): number {
return 2 ** depth * amount
}
export function getStampCostInBzz(depth: number, amount: number): number {
const BZZ_UNIT = 10 ** 16
return getStampCostInPlur(depth, amount) / BZZ_UNIT
}
// export function getStampTtlSeconds(amount: number, pricePerBlock = 24_000, blockTime = 5): number {
// return (amount * blockTime) / pricePerBlock
// }
export function getStampTtlSeconds(amount: number, pricePerBlock = 24_000, blockTime = 5): number {
return (amount * blockTime) / pricePerBlock
}