feat: add withdraw functionality and related enums

- Introduced WithdrawMethodEnum and ChainEnum in enum.ts for withdrawal methods and blockchain types.
- Updated types.ts to include WithdrawBody type for withdrawal requests.
- Created a new useResetRef composable for managing form state resets.
- Added a withdraw page with form handling in index.vue, including validation and submission logic.
- Integrated the new withdraw functionality into the wallet card component.
- Updated the main.ts file to include Pinia for state management.
- Created a wallet store to manage user balances.
- Modified the deposit page to improve user experience and validation.
- Added number pattern validation for input fields.
- Updated the router to include a new route for the withdraw page.
- Refactored input-label component styles for better layout.
- Added a new rules.ts file for future validation rules.
This commit is contained in:
2025-12-14 18:31:57 +07:00
parent 49414095f1
commit 28ddf12d45
22 changed files with 1838 additions and 95 deletions

View File

@@ -1,5 +1,7 @@
<script lang='ts' setup>
import type { DepositFiatBody } from "@/api/types";
import { toastController } from "@ionic/vue";
import { client } from "@/api";
import { AssetCodeEnum, PaymentChannelEnum } from "@/api/enum";
const form = ref<DepositFiatBody>({
@@ -19,11 +21,24 @@ function validate(value: string) {
if (value === "") {
return false;
}
const isEmailValid = emailPattern.test(form.value.amount);
const isNumber = numberPattern.test(value);
isEmailValid ? inputInstance.value?.$el.classList.add("ion-valid") : inputInstance.value?.$el.classList.add("ion-invalid");
isNumber ? inputInstance.value?.$el.classList.add("ion-valid") : inputInstance.value?.$el.classList.add("ion-invalid");
return isEmailValid;
return isNumber;
}
async function onSubmit() {
const { data, status } = await client.api.deposit.fiat.post(form.value);
if (status === 200) {
const toast = await toastController.create({
message: "Submission successful!",
duration: 1500,
position: "bottom",
});
await toast.present();
}
}
</script>
@@ -38,7 +53,7 @@ function validate(value: string) {
</ion-toolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<div class="flex flex-col gap-10px">
<div class="flex flex-col gap-20px">
<ui-input-label
label="Recharge bank card account"
model-value="74321329321312"
@@ -47,11 +62,22 @@ function validate(value: string) {
disabled
/>
<ion-select v-model="form.assetCode" label="Select Currency" placeholder="Select One" label-placement="floating">
<ion-radio-group v-model="form.assetCode">
<ion-label class="text-sm">
Choose Currency
</ion-label>
<ion-item v-for="item in AssetCodeEnum" :key="item">
<ion-radio :value="item" justify="space-between">
{{ item }}
</ion-radio>
</ion-item>
</ion-radio-group>
<!-- <ion-select v-model="form.assetCode" label="Select Currency" placeholder="Select One" label-placement="floating">
<ion-select-option v-for="item in AssetCodeEnum" :key="item" :value="item">
{{ item }}
</ion-select-option>
</ion-select>
</ion-select> -->
<ui-input-label
ref="inputInstance"
@@ -59,10 +85,17 @@ function validate(value: string) {
label="Amount"
placeholder="Enter the amount"
type="number"
inputmode="numeric"
error-text="Please enter a valid amount."
@ion-input="validate($event.target.value as string)"
@ion-blur="markTouched"
/>
<ion-note>Please make sure to enter the correct amount. After submission, the funds will be credited to your account after review in the background.</ion-note>
<ion-button expand="block" @click="onSubmit">
Submit
</ion-button>
</div>
</IonContent>
</ion-page>

View File

@@ -1,20 +1,41 @@
<script lang='ts' setup>
import { onIonViewDidEnter, onIonViewWillEnter } from "@ionic/vue";
import { client } from "@/api";
import RechargeChannel from "./recharge-channel.vue";
const { t } = useI18n();
const { data } = await client.api.asset.balances.get();
const router = useRouter();
const { state } = useWalletStore();
const rechargeInstance = ref<ModalInstance>();
async function init() {
const { data } = await client.api.asset.balances.get();
state.balances = data;
}
function onCloseModal() {
rechargeInstance.value?.$el.dismiss(null, "confirm");
}
function handleWithdraw() {
router.push("/withdraw/index");
}
onMounted(() => {
init();
});
onIonViewWillEnter(() => {
init();
});
onIonViewDidEnter(() => {
init();
});
</script>
<template>
<div class="mt-20px">
<div class="grid grid-cols-2 gap-20px p-20px bg-[var(--ion-card-background)] rounded-t-6px">
<div v-for="item in data" :key="item.assetCode" class="flex flex-col gap-4px">
<div class="mt-20px shadow-md rounded-6px">
<div class="grid grid-cols-2 gap-20px p-20px bg-[var(--ion-card-background)]">
<div v-for="item in state.balances" :key="item.assetCode" class="flex flex-col gap-4px">
<div class="ion-text-uppercase text-xs color-text-400 font-500 tracking-0.4px">
{{ item.assetCode }}
</div>
@@ -24,26 +45,27 @@ function onCloseModal() {
</div>
</div>
<div class="px-10px pb-20px bg-[var(--ion-card-background)] rounded-b-6px">
<div class="px-10px pb-20px bg-[var(--ion-card-background)]">
<ion-buttons class="gap-10px" expand="block">
<ion-button id="open-modal" expand="block" fill="clear">
<ion-button id="open-recharge-modal" expand="block" fill="clear">
{{ t("wallet.recharge") }}
</ion-button>
<ion-button expand="block" fill="clear">
<ion-button expand="block" fill="clear" @click="handleWithdraw">
{{ t("wallet.withdraw") }}
</ion-button>
</ion-buttons>
</div>
</div>
<ion-modal ref="rechargeInstance" class="recharge-channel-modal" trigger="open-modal" :initial-breakpoint="1" :breakpoints="[0, 1]">
<ion-modal ref="rechargeInstance" class="recharge-channel-modal" trigger="open-recharge-modal" :initial-breakpoint="1" :breakpoints="[0, 1]">
<RechargeChannel @close="onCloseModal" />
</ion-modal>
</template>
<style scoped>
.recharge-channel-modal {
.recharge-channel-modal,
.withdraw-channel-modal {
--height: auto;
}
</style>

View File

@@ -0,0 +1,143 @@
<script lang='ts' setup>
import type { WithdrawBody } from "@/api/types";
import { toastController } from "@ionic/vue";
import { client } from "@/api";
import { AssetCodeEnum, ChainEnum, WithdrawMethodEnum } from "@/api/enum";
const amountInputInst = useTemplateRef<InputInstance>("amountInputInst");
const [form, resetForm] = useResetRef<WithdrawBody>({
assetCode: AssetCodeEnum.USDT,
amount: "",
withdrawMethod: WithdrawMethodEnum.BANK,
toAddress: "",
bankAccountId: "",
chain: "BEP20",
});
const { state } = useWalletStore();
const maxAmount = computed(() => {
const balance = state.balances?.find(item => item.assetCode === form.value.assetCode);
return balance ? balance.available : "0";
});
function markTouched() {
amountInputInst.value?.$el.classList.add("ion-touched");
}
function validate(value: string) {
amountInputInst.value?.$el.classList.remove("ion-valid");
amountInputInst.value?.$el.classList.remove("ion-invalid");
if (value === "") {
return false;
}
const isNumber = numberPattern.test(value);
isNumber ? amountInputInst.value?.$el.classList.add("ion-valid") : amountInputInst.value?.$el.classList.add("ion-invalid");
return isNumber;
}
function handleCurrentChange() {
form.value.amount = "";
}
async function onSubmit() {
const { data, status } = await client.api.asset.withdraw.post(form.value);
if (status === 200) {
const toast = await toastController.create({
message: "Submission successful!",
duration: 1500,
position: "bottom",
});
await toast.present();
resetForm();
}
}
</script>
<template>
<ion-page>
<IonHeader>
<ion-toolbar class="ui-toolbar">
<ion-buttons slot="start">
<ion-back-button />
</ion-buttons>
<ion-title>Withdraw</ion-title>
</ion-toolbar>
</IonHeader>
<IonContent :fullscreen="true" class="ion-padding">
<div class="flex flex-col gap-20px">
<ion-radio-group v-model="form.assetCode" @ion-change="handleCurrentChange">
<ion-label class="text-sm">
Choose Currency
</ion-label>
<ion-item v-for="item in AssetCodeEnum" :key="item">
<ion-radio :value="item" justify="space-between">
{{ item }}
</ion-radio>
</ion-item>
</ion-radio-group>
<ion-radio-group v-model="form.withdrawMethod">
<ion-label class="text-sm">
Choose Withdraw Method
</ion-label>
<ion-item v-for="item in WithdrawMethodEnum" :key="item">
<ion-radio :value="item" justify="space-between">
{{ item }}
</ion-radio>
</ion-item>
</ion-radio-group>
<ui-input-label
ref="amountInputInst"
v-model="form.amount"
label="Amount"
:placeholder="`Enter the amount (Max: ${maxAmount})`"
type="number"
inputmode="numeric"
error-text="Please enter a valid amount."
:max="maxAmount"
@ion-input="validate($event.target.value as string)"
@ion-blur="markTouched"
/>
<ui-input-label
v-if="form.withdrawMethod === WithdrawMethodEnum.BANK"
v-model="form.bankAccountId"
label="Bank Account ID"
placeholder="Enter the bank account ID"
type="text"
inputmode="text"
error-text="Please enter a valid bank account ID."
/>
<template v-else-if="form.withdrawMethod === WithdrawMethodEnum.CRYPTO">
<ion-radio-group v-model="form.chain">
<ion-label class="text-sm">
Choose Chain
</ion-label>
<ion-item v-for="item in ChainEnum" :key="item">
<ion-radio :value="item" justify="space-between">
{{ item }}
</ion-radio>
</ion-item>
</ion-radio-group>
<ui-input-label
v-model="form.toAddress"
label="Crypto Address"
placeholder="Enter the crypto address"
type="text"
inputmode="text"
error-text="Please enter a valid crypto address."
/>
</template>
<ion-button expand="block" @click="onSubmit">
Submit
</ion-button>
</div>
</IonContent>
</ion-page>
</template>
<style lang='css' scoped></style>

View File