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: add UiInputAmount and decimals validation #592

Merged
merged 3 commits into from
Aug 6, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 48 additions & 35 deletions apps/ui/src/components/Modal/SendToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,6 @@ const RECIPIENT_DEFINITION = {
examples: ['Address or ENS']
};

const formValidator = getValidator({
$async: true,
type: 'object',
title: 'TokenTransfer',
additionalProperties: false,
required: ['to'],
properties: {
to: RECIPIENT_DEFINITION
}
});

const props = defineProps<{
open: boolean;
address: string;
Expand All @@ -51,8 +40,8 @@ const searchInput: Ref<HTMLElement | null> = ref(null);
const form: {
to: string;
token: string;
amount: string | number;
value: string | number;
amount: string;
value: string;
} = reactive(clone(DEFAULT_FORM_STATE));

const showPicker = ref(false);
Expand Down Expand Up @@ -91,6 +80,27 @@ const currentToken = computed(() => {
return token;
});

const amountDefinition = computed(() => ({
type: 'string',
decimals: currentToken.value?.decimals ?? 0,
title: 'Amount',
examples: ['0']
}));

const formValidator = computed(() =>
getValidator({
$async: true,
type: 'object',
title: 'TokenTransfer',
additionalProperties: false,
required: ['to', 'amount'],
properties: {
to: RECIPIENT_DEFINITION,
amount: amountDefinition.value
}
})
);

const formValid = computed(
() =>
currentToken.value &&
Expand Down Expand Up @@ -124,23 +134,28 @@ function handlePickerClick(type: 'token' | 'contact') {
});
}

function handleAmountUpdate(value) {
function handleAmountUpdate(value: string) {
form.amount = value;

if (value === '') {
form.value = '';
} else if (currentToken.value) {
form.value = parseFloat((value * currentToken.value.price).toFixed(2));
const float = parseFloat(value.replace(',', '.'));
form.value = (float * currentToken.value.price).toFixed(2);
}
}

function handleValueUpdate(value) {
function handleValueUpdate(value: string) {
form.value = value;

if (value === '') {
form.amount = '';
} else if (currentToken.value) {
form.amount = parseFloat((value / currentToken.value.price).toFixed(6));
const decimals = Math.min(currentToken.value.decimals, 6);

const float = parseFloat(value.replace(',', '.'));
const parsed = (float / currentToken.value.price).toFixed(decimals);
form.amount = parseFloat(parsed) === 0 ? '0' : parsed;
}
}

Expand Down Expand Up @@ -195,16 +210,16 @@ watch([() => props.address, () => props.network], ([address, network]) => {
watch(currentToken, token => {
if (!token || form.amount === '') return;

const amount =
typeof form.amount === 'string' ? parseFloat(form.amount) : form.amount;
form.value = parseFloat((amount * token.price).toFixed(2));
const amount = parseFloat(form.amount.replace(',', '.'));
form.value = (amount * token.price).toFixed(2);
});

watchEffect(async () => {
formValidated.value = false;

formErrors.value = await formValidator.validateAsync({
to: form.to
formErrors.value = await formValidator.value.validateAsync({
to: form.to,
amount: form.amount
});
formValidated.value = true;
});
Expand Down Expand Up @@ -292,13 +307,10 @@ watchEffect(async () => {
</div>
<div class="flex gap-2.5">
<div class="relative w-full">
<UiInputNumber
<UiInputAmount
:model-value="form.amount"
:definition="{
type: 'number',
title: 'Amount',
examples: ['0']
}"
:definition="amountDefinition"
:error="formErrors.amount"
@update:model-value="handleAmountUpdate"
/>
<button
Expand All @@ -308,13 +320,14 @@ watchEffect(async () => {
v-text="'max'"
/>
</div>
<UiInputNumber
v-if="currentToken.price !== 0"
class="w-full"
:model-value="form.value"
:definition="{ type: 'number', title: 'USD', examples: ['0'] }"
@update:model-value="handleValueUpdate"
/>
<div class="w-full">
<UiInputAmount
v-if="currentToken.price !== 0"
:model-value="form.value"
:definition="{ type: 'number', title: 'USD', examples: ['0'] }"
@update:model-value="handleValueUpdate"
/>
</div>
</div>
</div>
<template v-if="!showPicker" #footer>
Expand Down
40 changes: 30 additions & 10 deletions apps/ui/src/components/Modal/StakeToken.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { formatUnits } from '@ethersproject/units';
import { ETH_CONTRACT } from '@/helpers/constants';
import { createStakeTokenTransaction } from '@/helpers/transactions';
import { clone } from '@/helpers/utils';
import { getValidator } from '@/helpers/validation';
import { NetworkID, Transaction } from '@/types';

const STAKING_CONTRACTS = {
Expand All @@ -21,6 +22,23 @@ const DEFAULT_FORM_STATE = {
amount: ''
};

const AMOUNT_DEFINITION = {
type: 'string',
decimals: 18,
title: 'Amount',
examples: ['0']
};

const validator = getValidator({
type: 'object',
title: 'TokenTransfer',
additionalProperties: false,
required: ['amount'],
properties: {
amount: AMOUNT_DEFINITION
}
});

const props = defineProps<{
open: boolean;
address: string;
Expand All @@ -36,12 +54,17 @@ const emit = defineEmits<{

const form: {
to: string;
amount: string | number;
amount: string;
} = reactive(clone(DEFAULT_FORM_STATE));

const { assetsMap, loadBalances } = useBalances();

const formValid = computed(() => form.amount !== '');
const formErrors = computed(() =>
validator.validate({
amount: form.amount
})
);
const formValid = computed(() => Object.keys(formErrors.value).length === 0);

const token = computed(() => {
let token = assetsMap.value?.get(ETH_CONTRACT);
Expand Down Expand Up @@ -73,7 +96,7 @@ function handleMaxClick() {
);
}

function handleAmountUpdate(value) {
function handleAmountUpdate(value: string) {
form.amount = value;
}

Expand Down Expand Up @@ -122,13 +145,10 @@ watch(
</div>
<template v-else>
<div class="relative w-full">
<UiInputNumber
<UiInputAmount
:model-value="form.amount"
:definition="{
type: 'number',
title: 'Stake',
examples: ['0']
}"
:definition="AMOUNT_DEFINITION"
:error="formErrors.amount"
@update:model-value="handleAmountUpdate"
/>
<button
Expand All @@ -145,7 +165,7 @@ watch(
</div>
</div>
<div class="relative w-full">
<UiInputNumber
<UiInputAmount
:model-value="form.amount"
disabled
:definition="{
Expand Down
72 changes: 72 additions & 0 deletions apps/ui/src/components/Ui/InputAmount.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script lang="ts">
export default {
inheritAttrs: false
};
</script>

<script setup lang="ts">
const model = defineModel<string>({
required: true
});

const props = defineProps<{
error?: string;
definition: any;
}>();

const dirty = ref(false);

const inputValue = computed(() => {
if (!model.value && !dirty.value && props.definition.default) {
return props.definition.default;
}

return model.value;
});

function handleInput(event: Event) {
const inputEvent = event as InputEvent;
const target = inputEvent.target as HTMLInputElement;
const value = target.value;

dirty.value = true;

if (value === '') {
model.value = '';
return;
}

if (!/^[0-9]*[.,]?[0-9]*$/.test(value)) {
target.value = model.value;
return;
}

model.value = value;
}

watch(model, () => {
dirty.value = true;
});
</script>

<template>
<UiWrapperInput
v-slot="{ id }"
:definition="definition"
:error="error"
:dirty="dirty"
:input-value-length="inputValue?.length"
>
<input
:id="id"
:value="inputValue"
type="text"
class="s-input"
pattern="^[0-9]*[.,]?[0-9]*$"
inputmode="decimal"
v-bind="$attrs"
:placeholder="definition.examples && definition.examples[0]"
@input="handleInput"
/>
</UiWrapperInput>
</template>
Loading
Loading