import { ReactElement, useCallback, useContext, useEffect, useRef, useState } from 'react' import './UpgradeDriveModal.scss' import '../../styles/global.scss' import { CustomDropdown } from '../CustomDropdown/CustomDropdown' import { Button } from '../Button/Button' import { createPortal } from 'react-dom' import DriveIcon from 'remixicon-react/HardDrive2LineIcon' import DatabaseIcon from 'remixicon-react/Database2LineIcon' import WalletIcon from 'remixicon-react/Wallet3LineIcon' import ExternalLinkIcon from 'remixicon-react/ExternalLinkLineIcon' import CalendarIcon from 'remixicon-react/CalendarLineIcon' import { desiredLifetimeOptions } from '../../constants/stamps' import { Context as BeeContext } from '../../../../providers/Bee' import { fromBytesConversion, getExpiryDateByLifetime } from '../../utils/common' import { Context as SettingsContext } from '../../../../providers/Settings' import { Context as FMContext } from '../../../../providers/FileManager' import { BatchId, BeeRequestOptions, BZZ, capacityBreakpoints, Duration, PostageBatch, RedundancyLevel, Size, Utils, } from '@ethersphere/bee-js' import { DriveInfo } from '@solarpunkltd/file-manager-lib' import { getHumanReadableFileSize } from '../../../../utils/file' import { Warning } from '@material-ui/icons' interface UpgradeDriveModalProps { stamp: PostageBatch drive: DriveInfo onCancelClick: () => void containerColor?: string setErrorMessage?: (error: string) => void } const defaultErasureCodeLevel = RedundancyLevel.OFF const encryption_off = 'ENCRYPTION_OFF' export function UpgradeDriveModal({ stamp, onCancelClick, containerColor, drive, setErrorMessage, }: UpgradeDriveModalProps): ReactElement { const { nodeAddresses, walletBalance } = useContext(BeeContext) const { beeApi } = useContext(SettingsContext) const { setShowError } = useContext(FMContext) const [isBalanceSufficient, setIsBalanceSufficient] = useState(true) const [capacity, setCapacity] = useState(Size.fromBytes(0)) const [capacityExtensionCost, setCapacityExtensionCost] = useState('') const [capacityIndex, setCapacityIndex] = useState(0) const [durationExtensionCost, setDurationExtensionCost] = useState('') const [lifetimeIndex, setLifetimeIndex] = useState(0) const [validityEndDate, setValidityEndDate] = useState(new Date()) const [sizeMarks, setSizeMarks] = useState<{ value: number; label: string }[]>([]) const [extensionCost, setExtensionCost] = useState('0') const [isSubmitting, setIsSubmitting] = useState(false) const modalRoot = document.querySelector('.fm-main') || document.body const isMountedRef = useRef(true) useEffect(() => { return () => { isMountedRef.current = false } }, []) const handleCapacityChange = (value: number, index: number) => { setCapacity(Size.fromBytes(value === -1 ? 0 : value)) setCapacityIndex(index) } const handleCostCalculation = useCallback( async ( batchId: BatchId, capacity: Size, duration: Duration, options: BeeRequestOptions | undefined, encryption: boolean, erasureCodeLevel: RedundancyLevel, isCapacityExtensionSet: boolean, isDurationExtensionSet: boolean, ) => { setIsBalanceSufficient(true) let cost: BZZ | undefined try { cost = await beeApi?.getExtensionCost(batchId, capacity, duration, options, encryption, erasureCodeLevel) } catch (e) { setErrorMessage?.('Failed to calculate extension cost') setShowError(true) return } const costText = cost ? cost.toSignificantDigits(2) : '0' if (!isMountedRef.current) return if ((walletBalance && cost && cost.gte(walletBalance.bzzBalance)) || !walletBalance) { setIsBalanceSufficient(false) } const bothExtensions = isCapacityExtensionSet && isDurationExtensionSet const capacityOnly = isCapacityExtensionSet && !isDurationExtensionSet const durationOnly = !isCapacityExtensionSet && isDurationExtensionSet const noExtensions = !isCapacityExtensionSet && !isDurationExtensionSet if (bothExtensions) { setCapacityExtensionCost('') setDurationExtensionCost('') } else if (capacityOnly) { setCapacityExtensionCost(costText) setDurationExtensionCost('0') } else if (durationOnly) { setCapacityExtensionCost('0') setDurationExtensionCost(costText) } else { setCapacityExtensionCost('0') setDurationExtensionCost('0') } setExtensionCost(noExtensions ? '0' : costText) }, [beeApi, walletBalance, setErrorMessage, setShowError], ) useEffect(() => { const fetchSizes = () => { const sizes = Array.from(Utils.getStampEffectiveBytesBreakpoints(false, defaultErasureCodeLevel).values()) const capacityValues = capacityBreakpoints[encryption_off][defaultErasureCodeLevel] const fromIndex = capacityValues.findIndex(item => item.batchDepth === stamp.depth) const newSizes = sizes.slice(fromIndex + 1) const updatedSizes = [ { value: -1, label: 'No additional storage (0 GB)' }, ...newSizes.map(size => ({ value: size, label: getHumanReadableFileSize(size - stamp.size.toBytes()), })), ] setSizeMarks(updatedSizes) } fetchSizes() }, [stamp.depth, stamp.size]) useEffect(() => { const fetchExtensionCost = () => { const isCapacitySet = capacityIndex > 0 const isDurationSet = true const duration = Duration.fromEndDate(validityEndDate) handleCostCalculation( stamp.batchID, capacity, duration, undefined, false, defaultErasureCodeLevel, isCapacitySet, isDurationSet, ) } fetchExtensionCost() }, [capacity, validityEndDate, capacityIndex, handleCostCalculation, lifetimeIndex, stamp.batchID]) useEffect(() => { setValidityEndDate(getExpiryDateByLifetime(lifetimeIndex, stamp.duration.toEndDate())) }, [lifetimeIndex, stamp.duration]) const batchIdStr = stamp.batchID.toString() const shortBatchId = batchIdStr.length > 12 ? `${batchIdStr.slice(0, 4)}...${batchIdStr.slice(-4)}` : batchIdStr return createPortal(
Upgrade {drive.name || stamp.label || shortBatchId}
Choose extension period and additional storage for your drive.
Wallet information
{walletBalance && nodeAddresses ? (
Balance
{`${walletBalance.bzzBalance.toSignificantDigits(4)} xBZZ`}
Wallet address:
{`${walletBalance.walletAddress.slice( 0, 4, )}...${walletBalance.walletAddress.slice(-4)}`}
) : (
Wallet information is not available
)}
Need help topping up?
} options={sizeMarks} value={capacityIndex === 0 ? -1 : capacity.toBytes()} onChange={handleCapacityChange} />
} options={desiredLifetimeOptions} value={lifetimeIndex} onChange={(value, index) => { setLifetimeIndex(value) }} />
Summary
Drive: {drive.name} {drive.isAdmin && }
BatchId: {stamp.label} ({shortBatchId})
Expiry: {stamp.duration.toEndDate().toLocaleDateString()}
Additional storage:{' '} {(() => { if (capacityIndex === 0) return '0 GB' return `${ fromBytesConversion(Math.max(capacity.toBytes() - stamp.size.toBytes(), 0), 'GB').toFixed(3) + ' GB' } ${durationExtensionCost === '' ? '' : '(' + extensionCost + ' xBZZ)'}` })()}
Extension period:{' '} {`${desiredLifetimeOptions[lifetimeIndex]?.label} ${ capacityExtensionCost === '' ? '' : '(' + extensionCost + ' xBZZ)' }`}
Total:{' '} {extensionCost} xBZZ {isBalanceSufficient ? '' : '(Insufficient balance)'}
{isSubmitting && (
Please wait…
)}
, modalRoot, ) }