Skip to content

Commit

Permalink
chore: process amount base on decimal information (#905)
Browse files Browse the repository at this point in the history
* chore: process amount base on decimal information

* chore: update the amount input description

---------

Co-authored-by: Oleksandr Raspopov <[email protected]>
  • Loading branch information
Alexander-frenki and Oleksandr Raspopov authored Jan 27, 2025
1 parent bedf727 commit bae1f7c
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 77 deletions.
1 change: 1 addition & 0 deletions ui/src/adapters/api/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const paymentConfigurationsParser = getStrictParser<PaymentConfigurations
ChainID: z.number(),
PaymentOption: z.object({
ContractAddress: z.string(),
Decimals: z.number(),
Name: z.string(),
Type: z.string(),
}),
Expand Down
5 changes: 4 additions & 1 deletion ui/src/adapters/parsers/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ export const credentialFormParser = getStrictParser<

export type PaymentConfigFormData = {
amount: string;
decimals: number;
paymentOptionID: string;
recipient: string;
signingKeyID: string;
Expand All @@ -332,6 +333,7 @@ export const paymentOptionFormParser = getStrictParser<
paymentOptions: z.array(
z.object({
amount: z.string(),
decimals: z.number(),
paymentOptionID: z
.string()
.refine((value) => !isNaN(Number(value)), { message: "Must be a valid number" }),
Expand All @@ -343,8 +345,9 @@ export const paymentOptionFormParser = getStrictParser<
.transform(({ description, name, paymentOptions }) => ({
description,
name,
paymentOptions: paymentOptions.map(({ paymentOptionID, ...other }) => ({
paymentOptions: paymentOptions.map(({ amount, decimals, paymentOptionID, ...other }) => ({
...other,
amount: (parseFloat(amount) * Math.pow(10, decimals)).toString(),
paymentOptionID: parseInt(paymentOptionID),
})),
}))
Expand Down
91 changes: 82 additions & 9 deletions ui/src/components/payments/CreatePaymentOption.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import { App, Card } from "antd";
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

import { createPaymentOption } from "src/adapters/api/payments";
import { createPaymentOption, getPaymentConfigurations } from "src/adapters/api/payments";
import { notifyParseError } from "src/adapters/parsers";
import { PaymentOptionFormData, paymentOptionFormParser } from "src/adapters/parsers/view";
import { PaymentOptionForm } from "src/components/payments/PaymentOptionForm";
import { ErrorResult } from "src/components/shared/ErrorResult";
import { LoadingResult } from "src/components/shared/LoadingResult";
import { SiderLayoutContent } from "src/components/shared/SiderLayoutContent";
import { useEnvContext } from "src/contexts/Env";
import { useIdentityContext } from "src/contexts/Identity";
import { AppError, PaymentConfigurations } from "src/domain";
import { ROUTES } from "src/routes";
import {
AsyncTask,
hasAsyncTaskFailed,
isAsyncTaskDataAvailable,
isAsyncTaskStarting,
} from "src/utils/async";
import { isAbortedError, makeRequestAbortable } from "src/utils/browser";

import { PAYMENT_OPTIONS_ADD_NEW } from "src/utils/constants";

Expand All @@ -19,6 +30,44 @@ export function CreatePaymentOption() {
const navigate = useNavigate();
const { message } = App.useApp();

const [paymentConfigurations, setPaymentConfigurations] = useState<
AsyncTask<PaymentConfigurations, AppError>
>({
status: "pending",
});

const fetchPaymentConfigurations = useCallback(
async (signal?: AbortSignal) => {
setPaymentConfigurations((previousConfigurations) =>
isAsyncTaskDataAvailable(previousConfigurations)
? { data: previousConfigurations.data, status: "reloading" }
: { status: "loading" }
);

const response = await getPaymentConfigurations({
env,
signal,
});
if (response.success) {
setPaymentConfigurations({
data: response.data,
status: "successful",
});
} else {
if (!isAbortedError(response.error)) {
setPaymentConfigurations({ error: response.error, status: "failed" });
}
}
},
[env]
);

useEffect(() => {
const { aborter } = makeRequestAbortable(fetchPaymentConfigurations);

return aborter;
}, [fetchPaymentConfigurations]);

const handleSubmit = (formValues: PaymentOptionFormData) => {
const parsedFormData = paymentOptionFormParser.safeParse(formValues);

Expand Down Expand Up @@ -48,14 +97,38 @@ export function CreatePaymentOption() {
title={PAYMENT_OPTIONS_ADD_NEW}
>
<Card className="centered" title="Payment option details">
<PaymentOptionForm
initialValies={{
description: "",
name: "",
paymentOptions: [],
}}
onSubmit={handleSubmit}
/>
{(() => {
if (hasAsyncTaskFailed(paymentConfigurations)) {
return (
<Card className="centered">
<ErrorResult
error={[
"An error occurred while downloading a payments configuration from the API:",
paymentConfigurations.error.message,
].join("\n")}
/>
</Card>
);
} else if (isAsyncTaskStarting(paymentConfigurations)) {
return (
<Card className="centered">
<LoadingResult />
</Card>
);
} else {
return (
<PaymentOptionForm
initialValies={{
description: "",
name: "",
paymentOptions: [],
}}
onSubmit={handleSubmit}
paymentConfigurations={paymentConfigurations.data}
/>
);
}
})()}
</Card>
</SiderLayoutContent>
);
Expand Down
121 changes: 103 additions & 18 deletions ui/src/components/payments/PaymentOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { useNavigate, useParams } from "react-router-dom";
import { useIdentityContext } from "../../contexts/Identity";
import {
deletePaymentOption,
getPaymentConfigurations,
getPaymentOption,
updatePaymentOption,
} from "src/adapters/api/payments";
import { notifyParseError } from "src/adapters/parsers";
import { buildAppError, notifyError, notifyParseError } from "src/adapters/parsers";
import { PaymentOptionFormData, paymentOptionFormParser } from "src/adapters/parsers/view";
import IconDots from "src/assets/icons/dots-vertical.svg?react";
import EditIcon from "src/assets/icons/edit-02.svg?react";
Expand All @@ -21,9 +22,14 @@ import { ErrorResult } from "src/components/shared/ErrorResult";
import { LoadingResult } from "src/components/shared/LoadingResult";
import { SiderLayoutContent } from "src/components/shared/SiderLayoutContent";
import { useEnvContext } from "src/contexts/Env";
import { AppError, PaymentOption as PaymentOptionType } from "src/domain";
import { AppError, PaymentConfigurations, PaymentOption as PaymentOptionType } from "src/domain";
import { ROUTES } from "src/routes";
import { AsyncTask, hasAsyncTaskFailed, isAsyncTaskStarting } from "src/utils/async";
import {
AsyncTask,
hasAsyncTaskFailed,
isAsyncTaskDataAvailable,
isAsyncTaskStarting,
} from "src/utils/async";
import { isAbortedError, makeRequestAbortable } from "src/utils/browser";
import { PAYMENT_OPTIONS_DETAILS } from "src/utils/constants";
import { formatDate } from "src/utils/forms";
Expand All @@ -39,8 +45,40 @@ export function PaymentOption() {
status: "pending",
});

const [paymentConfigurations, setPaymentConfigurations] = useState<
AsyncTask<PaymentConfigurations, AppError>
>({
status: "pending",
});

const { paymentOptionID } = useParams();

const fetchPaymentConfigurations = useCallback(
async (signal?: AbortSignal) => {
setPaymentConfigurations((previousConfigurations) =>
isAsyncTaskDataAvailable(previousConfigurations)
? { data: previousConfigurations.data, status: "reloading" }
: { status: "loading" }
);

const response = await getPaymentConfigurations({
env,
signal,
});
if (response.success) {
setPaymentConfigurations({
data: response.data,
status: "successful",
});
} else {
if (!isAbortedError(response.error)) {
setPaymentConfigurations({ error: response.error, status: "failed" });
}
}
},
[env]
);

const fetchPaymentOption = useCallback(
async (signal?: AbortSignal) => {
if (paymentOptionID) {
Expand All @@ -55,14 +93,15 @@ export function PaymentOption() {

if (response.success) {
setPaymentOption({ data: response.data, status: "successful" });
void fetchPaymentConfigurations(signal);
} else {
if (!isAbortedError(response.error)) {
setPaymentOption({ error: response.error, status: "failed" });
}
}
}
},
[env, paymentOptionID, identifier]
[env, paymentOptionID, identifier, fetchPaymentConfigurations]
);

useEffect(() => {
Expand Down Expand Up @@ -118,18 +157,31 @@ export function PaymentOption() {
title={PAYMENT_OPTIONS_DETAILS}
>
{(() => {
if (hasAsyncTaskFailed(paymentOption)) {
if (hasAsyncTaskFailed(paymentOption) || hasAsyncTaskFailed(paymentConfigurations)) {
return (
<Card className="centered">
<ErrorResult
error={[
"An error occurred while downloading a payment option from the API:",
paymentOption.error.message,
].join("\n")}
/>
{hasAsyncTaskFailed(paymentOption) && (
<ErrorResult
error={[
"An error occurred while downloading a payment option from the API:",
paymentOption.error.message,
].join("\n")}
/>
)}
{hasAsyncTaskFailed(paymentConfigurations) && (
<ErrorResult
error={[
"An error occurred while downloading a payments configuration from the API:",
paymentConfigurations.error.message,
].join("\n")}
/>
)}
</Card>
);
} else if (isAsyncTaskStarting(paymentOption)) {
} else if (
isAsyncTaskStarting(paymentOption) ||
isAsyncTaskStarting(paymentConfigurations)
) {
return (
<Card className="centered">
<LoadingResult />
Expand Down Expand Up @@ -201,7 +253,29 @@ export function PaymentOption() {
</Card>

<PaymentConfigTable
configs={paymentOption.data.paymentOptions}
configs={paymentOption.data.paymentOptions.map(
({ amount, paymentOptionID, ...other }) => {
const configuration = paymentConfigurations.data[paymentOptionID];
if (!configuration) {
void notifyError(
buildAppError(
`Can't find payment configuration for ID: ${paymentOptionID}`
)
);
}

return {
amount: configuration
? (
parseFloat(amount) /
Math.pow(10, configuration.PaymentOption.Decimals)
).toString()
: amount,
paymentOptionID,
...other,
};
}
)}
showTitle={true}
/>
</Flex>
Expand All @@ -216,14 +290,25 @@ export function PaymentOption() {
initialValies={{
description: paymentOption.data.description,
name: paymentOption.data.name,
paymentOptions: paymentOption.data.paymentOptions.map(
({ paymentOptionID, ...other }) => ({
paymentOptionID: paymentOptionID.toString(),
...other,
paymentOptions: paymentOption.data.paymentOptions
.map(({ amount, paymentOptionID, ...other }) => {
const configuration = paymentConfigurations.data[paymentOptionID];
return configuration
? {
amount: (
parseFloat(amount) /
Math.pow(10, configuration.PaymentOption.Decimals)
).toString(),
decimals: configuration.PaymentOption.Decimals,
paymentOptionID: paymentOptionID.toString(),
...other,
}
: null;
})
),
.filter((option) => !!option),
}}
onSubmit={handleEdit}
paymentConfigurations={paymentConfigurations.data}
/>
</EditModal>
</>
Expand Down
Loading

0 comments on commit bae1f7c

Please sign in to comment.