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

fix: Amount input correctly handles numbers and dots #481

Merged
merged 6 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 29 additions & 25 deletions packages/shared/components/inputs/Amount.svelte
@@ -1,6 +1,7 @@
<script lang="typescript">
import { Unit } from '@iota/unit-converter'
import { Input, Text } from 'shared/components'
import { convertUnitsNoE } from 'shared/lib/units'

export let amount = undefined
export let unit = Unit.Mi
Expand All @@ -10,20 +11,17 @@
export let maxClick = () => {}
export let error = ''
export let disabled = false
export let autofocus = false

const Units = Object.values(Unit).filter((x) => x !== 'Pi')

let dropdown = false

function onKey(e) {
if (e.keyCode === 8 || e.target.value.length <= 12) {
return true
} else {
e.target.value = e.target.value.substring(0, 12)
}
}
const clickOutside = () => {
dropdown = false
}
const onSelect = (index) => {
amount = convertUnitsNoE(amount, unit, index)
unit = index
}
</script>
Expand All @@ -32,10 +30,6 @@
amount-input {
nav {
border-radius: 10px;
&.active {
@apply opacity-100;
@apply pointer-events-auto;
}
button {
&:hover,
&.active {
Expand All @@ -51,31 +45,41 @@
<amount-input class="relative block {classes}">
<Input
{error}
type="number"
placeholder={label || locale('general.amount')}
on:keydown={onKey}
bind:value={amount}
{disabled} />
maxlength={17}
{disabled}
{autofocus}
integer={unit === Unit.i}
float={unit !== Unit.i} />
<actions class="absolute right-0 top-2.5 h-8 flex flex-row items-center text-12 text-gray-500 dark:text-white">
<button on:click={maxClick} class={`pr-3 ${disabled ? "cursor-auto" : "hover:text-blue-500 cursor-pointer"}`} {disabled}>{locale('actions.max').toUpperCase()}</button>
<button
on:click={maxClick}
class={`pr-3 ${disabled ? 'cursor-auto' : 'hover:text-blue-500 focus:text-blue-500 cursor-pointer'}`}
{disabled}>{locale('actions.max').toUpperCase()}</button>
<button
on:click={(e) => {
e.preventDefault()
e.stopPropagation()
dropdown = !dropdown
}}
class={`w-10 h-full text-center px-2 border-l border-solid border-gray-500 ${disabled ? "cursor-auto" : "cursor-pointer"}`}
class={`w-10 h-full text-center px-2 border-l border-solid border-gray-500 ${disabled ? 'cursor-auto' : 'hover:text-blue-500 focus:text-blue-500 cursor-pointer'}`}
{disabled}>
{unit}
<nav
class:active={dropdown && !disabled}
class="absolute w-10 overflow-y-auto bg-white border border-solid border-gray-500 pointer-events-none opacity-0 z-10 text-left top-10 right-0">
{#each Object.values(Unit) as _unit}
<button class="text-center w-full py-2" on:click={() => onSelect(_unit)} class:active={unit === _unit}>
<Text type="p" smaller>{_unit}</Text>
</button>
{/each}
</nav>
{#if !disabled && dropdown}
<nav
class="absolute w-10 overflow-y-auto bg-white border border-solid border-gray-500 z-10 text-left top-10 right-0">
{#each Units as _unit}
<button
id={_unit}
class="text-center w-full py-2"
on:click={() => onSelect(_unit)}
class:active={unit === _unit}>
<Text type="p" smaller>{_unit}</Text>
</button>
{/each}
</nav>
{/if}
</button>
</actions>
</amount-input>
32 changes: 23 additions & 9 deletions packages/shared/components/inputs/Input.svelte
Expand Up @@ -8,24 +8,38 @@
export let type = 'text'
export let error
export let maxlength = null
export let numeric = false
export let float = false
export let integer = false
export let autofocus = false
export let submitHandler = undefined
export let disabled = false

let inputElement

const handleInput = (event) => {
value = event.target.value
const handleInput = (e) => {
value = e.target.value
}

const onKeyPress = (e) => {
// if the input is numeric, we accept only numbers and enter press
if (numeric && e.keyCode !== 13 && (e.which < 48 || e.which > 57)) {
e.preventDefault()
}
if (e.keyCode === 13 && submitHandler) {
submitHandler()
if (e.keyCode !== 8) {
const isReturn = e.keyCode === 13
if (isReturn && submitHandler) {
submitHandler()
}
if (maxlength && value.length >= maxlength) {
e.preventDefault()
}
if ((float || integer) && !isReturn) {
// if the input is float, we accept one dot
if (float && (e.keyCode === 46 || e.keyCode === 190)) {
if (value.indexOf('.') >= 0) {
e.preventDefault()
}
} else if (e.keyCode < 48 || e.keyCode > 57) {
// if float or interger we accept numbers
e.preventDefault()
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/shared/components/inputs/Password.svelte
Expand Up @@ -13,7 +13,7 @@
export let locale = undefined
export let maxlength = undefined
export let error = null
export let numeric = false
export let integer = false
export let autofocus = false
export let submitHandler = undefined
export let disabled = false
Expand Down Expand Up @@ -61,7 +61,7 @@
{type}
bind:value
{maxlength}
{numeric}
{integer}
{autofocus}
{disabled}
placeholder={placeholder || locale('general.password')}
Expand Down
29 changes: 29 additions & 0 deletions packages/shared/lib/units.ts
@@ -1,4 +1,8 @@
import { Unit, convertUnits } from '@iota/unit-converter'
import Big from 'big.js'

// Set this to avoid small numbers switching to exponential format
Big.NE = -20

/**
* IOTA Units Map
Expand Down Expand Up @@ -64,3 +68,28 @@ const getUnit = (value: number): Unit => {

return bestUnits
}

/**
* Convert the value to different units.
* @param value The value to convert.
* @param fromUnit The form unit.
* @param toUnit The to unit.
* @returns The formatted unit.
*/
export const convertUnitsNoE = (value: number, fromUnit: Unit, toUnit: Unit): string => {
if (value === 0) {
return "0";
}

if (!value) {
return "";
}

if (fromUnit === toUnit) {
return value.toString();
}

const scaledValue = new Big(value).times(UNIT_MAP[fromUnit].val).div(UNIT_MAP[toUnit].val)

return toUnit === Unit.i ? scaledValue.toFixed(0) : scaledValue.toString();
}
1 change: 1 addition & 0 deletions packages/shared/package.json
Expand Up @@ -4,6 +4,7 @@
"author": "IOTA Foundation <contact@iota.org>",
"license": "Apache-2.0",
"dependencies": {
"big.js": "^6.0.3",
"@iota/unit-converter": "^1.0.0-beta.30",
"chart.js": "^2.9.3",
"qr.js": "0.0.0",
Expand Down
Expand Up @@ -345,7 +345,7 @@
showRevealToggle
{locale}
maxlength="6"
numeric
integer
placeholder={locale('views.settings.changePincode.currentPincode')}
disabled={pinCodeBusy} />
<Password
Expand All @@ -355,7 +355,7 @@
showRevealToggle
{locale}
maxlength="6"
numeric
integer
placeholder={locale('views.settings.changePincode.newPincode')}
disabled={pinCodeBusy} />
<Password
Expand All @@ -364,7 +364,7 @@
showRevealToggle
{locale}
maxlength="6"
numeric
integer
placeholder={locale('views.settings.changePincode.confirmNewPincode')}
disabled={pinCodeBusy} />
<div class="flex flex-row items-center">
Expand Down
68 changes: 38 additions & 30 deletions packages/shared/routes/dashboard/wallet/views/Send.svelte
Expand Up @@ -5,6 +5,7 @@
import { accountRoute, walletRoute } from 'shared/lib/router'
import type { TransferProgressEventType } from 'shared/lib/typings/events'
import { AccountRoutes, WalletRoutes } from 'shared/lib/typings/routes'
import { convertUnitsNoE } from 'shared/lib/units'
import { ADDRESS_LENGTH, validateBech32Address } from 'shared/lib/utils'
import { isTransferring, transferState, WalletAccount } from 'shared/lib/wallet'
import { getContext, onMount } from 'svelte'
Expand All @@ -24,12 +25,15 @@

let selectedSendType = SEND_TYPE.EXTERNAL
let unit = Unit.Mi
let amount = convertUnits($sendParams.amount, Unit.i, unit).toString()
let amount = $sendParams.amount === 0 ? '' : convertUnitsNoE($sendParams.amount, Unit.i, unit)
let to = undefined
let amountError = ''
let addressPrefix = ($account ?? $accounts[0]).depositAddress.split('1')[0]
let addressError = ''

// This looks odd but sets a reactive dependency on amount, so when it changes the error will clear
$: amount, amountError = ''

let transferSteps: {
[key in TransferProgressEventType | 'Complete']: {
label: string
Expand Down Expand Up @@ -95,38 +99,41 @@
amountError = locale('error.send.amountNoFloat')
} else {
let amountAsFloat = Number.parseFloat(amount)
if (amountAsFloat.toString() !== amount) {
if (Number.isNaN(amountAsFloat)) {
amountError = locale('error.send.amountInvalidFormat')
} else if (amountAsFloat > from.balance) {
amountError = locale('error.send.amountTooHigh')
} else if (amountAsFloat <= 0) {
amountError = locale('error.send.amountZero')
}
} else {
const amountAsI = convertUnits(amountAsFloat, unit, Unit.i)
if (amountAsI > from.balance) {
amountError = locale('error.send.amountTooHigh')
} else if (amountAsI <= 0) {
amountError = locale('error.send.amountZero')
}

if (selectedSendType === SEND_TYPE.EXTERNAL) {
// Validate address length
if ($sendParams.address.length !== ADDRESS_LENGTH) {
addressError = locale('error.send.addressLength', {
values: {
length: ADDRESS_LENGTH,
},
})
} else if (!validateBech32Address(addressPrefix, $sendParams.address)) {
addressError = locale('error.send.wrongAddressFormat', {
values: {
prefix: addressPrefix,
},
})
if (selectedSendType === SEND_TYPE.EXTERNAL) {
// Validate address length
if ($sendParams.address.length !== ADDRESS_LENGTH) {
addressError = locale('error.send.addressLength', {
values: {
length: ADDRESS_LENGTH,
},
})
} else if (!validateBech32Address(addressPrefix, $sendParams.address)) {
addressError = locale('error.send.wrongAddressFormat', {
values: {
prefix: addressPrefix,
},
})
}
}
}

if (!amountError && !addressError) {
$sendParams.amount = convertUnits(amountAsFloat, unit, Unit.i)
if (!amountError && !addressError) {
$sendParams.amount = amountAsI

if (selectedSendType === SEND_TYPE.INTERNAL) {
internalTransfer(from.id, to.id, $sendParams.amount)
} else {
send(from.id, $sendParams.address, $sendParams.amount)
if (selectedSendType === SEND_TYPE.INTERNAL) {
internalTransfer(from.id, to.id, $sendParams.amount)
} else {
send(from.id, $sendParams.address, $sendParams.amount)
}
}
}
}
Expand All @@ -147,7 +154,7 @@
}
}
const handleMaxClick = () => {
amount = convertUnits(from.balance, Unit.i, unit).toString()
amount = convertUnitsNoE(from.balance, Unit.i, unit)
}
onMount(() => {
to = $accounts.length === 2 ? accountsDropdownItems[from.id === $accounts[0].id ? 1 : 0] : to
Expand Down Expand Up @@ -196,7 +203,8 @@
maxClick={handleMaxClick}
{locale}
classes="mb-2"
disabled={$isTransferring} />
disabled={$isTransferring}
autofocus />
{#if selectedSendType === SEND_TYPE.INTERNAL}
<Dropdown
value={to?.label || ''}
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -1622,6 +1622,11 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==

big.js@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.0.3.tgz#8b4d99ac7023668e0e465d3f78c23b8ac29ad381"
integrity sha512-n6yn1FyVL1EW2DBAr4jlU/kObhRzmr+NNRESl65VIOT8WBJj/Kezpx2zFdhJUqYI6qrtTW7moCStYL5VxeVdPA==

bignumber.js@*:
version "9.0.1"
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5"
Expand Down