Skip to content

Commit

Permalink
chore: ui for payment requests
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksandr Raspopov committed Jan 28, 2025
1 parent dc677eb commit 0d29fa1
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 9 deletions.
93 changes: 90 additions & 3 deletions ui/src/adapters/api/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,21 @@ import { z } from "zod";

import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters";
import { ID, IDParser, Message, buildAuthorizationHeader, messageParser } from "src/adapters/api";
import { datetimeParser, getResourceParser, getStrictParser } from "src/adapters/parsers";
import { Env, PaymentConfigurations, PaymentOption } from "src/domain";
import {
datetimeParser,
getListParser,
getResourceParser,
getStrictParser,
} from "src/adapters/parsers";
import {
Env,
PaymentConfigurations,
PaymentOption,
PaymentRequest,
PaymentRequestStatus,
} from "src/domain";
import { API_VERSION } from "src/utils/constants";
import { Resource } from "src/utils/types";
import { List, Resource } from "src/utils/types";

type PaymentOptionInput = Omit<PaymentOption, "modifiedAt" | "createdAt"> & {
createdAt: string;
Expand All @@ -32,6 +43,30 @@ export const paymentOptionParser = getStrictParser<PaymentOptionInput, PaymentOp
})
);

type PaymentRequestInput = Omit<PaymentRequest, "createdAt" | "modifiedAt"> & {
createdAt: string;
modifiedAt: string;
};

export const paymentRequestParser = getStrictParser<PaymentRequestInput, PaymentRequest>()(
z.object({
createdAt: datetimeParser,
id: z.string(),
modifiedAt: datetimeParser,
paymentOptionID: z.string(),
payments: z.union([
z.object({}).catchall(z.any()),
z.array(z.any()),
z.string(),
z.number(),
z.boolean(),
z.null(),
]),
status: z.nativeEnum(PaymentRequestStatus),
userDID: z.string(),
})
);

export const paymentConfigurationsParser = getStrictParser<PaymentConfigurations>()(
z.record(
z.object({
Expand Down Expand Up @@ -209,3 +244,55 @@ export async function getPaymentConfigurations({
return buildErrorResponse(error);
}
}

export async function getPaymentRequests({
env,
identifier,
signal,
}: {
env: Env;
identifier: string;
signal?: AbortSignal;
}): Promise<Response<List<PaymentRequest>>> {
try {
const response = await axios({
baseURL: env.api.url,
headers: {
Authorization: buildAuthorizationHeader(env),
},
method: "GET",
signal,
url: `${API_VERSION}/identities/${identifier}/payment-request`,
});
return buildSuccessResponse(getListParser(paymentRequestParser).parse(response.data));
} catch (error) {
return buildErrorResponse(error);
}
}

export async function getPaymentRequest({
env,
identifier,
paymentRequestID,
signal,
}: {
env: Env;
identifier: string;
paymentRequestID: string;
signal?: AbortSignal;
}): Promise<Response<PaymentRequest>> {
try {
const response = await axios({
baseURL: env.api.url,
headers: {
Authorization: buildAuthorizationHeader(env),
},
method: "GET",
signal,
url: `${API_VERSION}/identities/${identifier}/payment-request/${paymentRequestID}`,
});
return buildSuccessResponse(paymentRequestParser.parse(response.data));
} catch (error) {
return buildErrorResponse(error);
}
}
6 changes: 5 additions & 1 deletion ui/src/components/credentials/CredentialDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ export function CredentialDetails() {
</Card>
);
} else if (hasAsyncTaskFailed(issuedMessages)) {
return <ErrorResult error={issuedMessages.error.message} />;
return (
<Card className="centered">
<ErrorResult error={issuedMessages.error.message} />
</Card>
);
} else if (hasAsyncTaskFailed(credentialSubjectValue)) {
return (
<Card className="centered">
Expand Down
202 changes: 202 additions & 0 deletions ui/src/components/payments/PaymentRequest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { Card, Flex, Space, Typography } from "antd";
import { useCallback, useEffect, useState } from "react";
import { generatePath, useParams } from "react-router-dom";

import { useIdentityContext } from "../../contexts/Identity";
import { getPaymentOptions, getPaymentRequest } from "src/adapters/api/payments";
import { notifyErrors } from "src/adapters/parsers";
import { JSONHighlighter } from "src/components/schemas/JSONHighlighter";
import { Detail } from "src/components/shared/Detail";
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, PaymentRequest as PaymentRequestType } 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_REQUESTS_DETAILS } from "src/utils/constants";
import { formatDate } from "src/utils/forms";

export function PaymentRequest() {
const env = useEnvContext();
const { identifier } = useIdentityContext();

const [paymentRequest, setPaymentRequest] = useState<AsyncTask<PaymentRequestType, AppError>>({
status: "pending",
});

const [paymentOptions, setPaymentOptions] = useState<AsyncTask<PaymentOption[], AppError>>({
status: "pending",
});

const { paymentRequestID } = useParams();

const fetchPaymentRequest = useCallback(
async (signal?: AbortSignal) => {
if (paymentRequestID) {
setPaymentRequest({ status: "loading" });

const response = await getPaymentRequest({
env,
identifier,
paymentRequestID,
signal,
});

if (response.success) {
setPaymentRequest({ data: response.data, status: "successful" });
} else {
if (!isAbortedError(response.error)) {
setPaymentRequest({ error: response.error, status: "failed" });
}
}
}
},
[env, paymentRequestID, identifier]
);

const fetchPaymentOptions = useCallback(
async (signal?: AbortSignal) => {
setPaymentOptions((previousPaymentOptions) =>
isAsyncTaskDataAvailable(previousPaymentOptions)
? { data: previousPaymentOptions.data, status: "reloading" }
: { status: "loading" }
);

const response = await getPaymentOptions({
env,
identifier,
params: {},
signal,
});
if (response.success) {
setPaymentOptions({
data: response.data.items.successful,
status: "successful",
});

void notifyErrors(response.data.items.failed);
} else {
if (!isAbortedError(response.error)) {
setPaymentOptions({ error: response.error, status: "failed" });
}
}
},
[env, identifier]
);
useEffect(() => {
const { aborter } = makeRequestAbortable(fetchPaymentRequest);

return aborter;
}, [fetchPaymentRequest]);

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

return aborter;
}, [fetchPaymentOptions]);

if (!paymentRequestID) {
return <ErrorResult error="No payment request provided." />;
}

return (
<SiderLayoutContent
description="View payment request details"
showBackButton
showDivider
title={PAYMENT_REQUESTS_DETAILS}
>
{(() => {
if (hasAsyncTaskFailed(paymentRequest)) {
return (
<Card className="centered">
<ErrorResult
error={[
"An error occurred while downloading a payment request from the API:",
paymentRequest.error.message,
].join("\n")}
/>
</Card>
);
} else if (hasAsyncTaskFailed(paymentOptions)) {
return (
<Card className="centered">
<ErrorResult
error={[
"An error occurred while downloading a payment options from the API:",
paymentOptions.error.message,
].join("\n")}
/>
</Card>
);
} else if (isAsyncTaskStarting(paymentRequest) || isAsyncTaskStarting(paymentOptions)) {
return (
<Card className="centered">
<LoadingResult />
</Card>
);
} else {
const paymentOptionName = paymentOptions.data.find(
({ id }) => id === paymentRequest.data.paymentOptionID
)?.name;

return (
<>
<Card
className="centered"
title={
<Flex align="center" gap={8} justify="space-between">
<Typography.Text style={{ fontWeight: 600 }}>
{paymentRequest.data.id}
</Typography.Text>
</Flex>
}
>
<Flex gap={24} vertical>
<Card className="background-grey">
<Space direction="vertical">
<Detail
copyable
ellipsisPosition={5}
label="User DID"
text={paymentRequest.data.userDID}
/>
<Detail
label="Created date"
text={formatDate(paymentRequest.data.createdAt)}
/>
<Detail
label="Modified date"
text={formatDate(paymentRequest.data.modifiedAt)}
/>
<Detail label="Status" text={paymentRequest.data.status} />
<Detail
href={generatePath(ROUTES.paymentOptionDetails.path, {
paymentOptionID: paymentRequest.data.paymentOptionID,
})}
label="Payment option"
text={paymentOptionName || paymentRequest.data.paymentOptionID}
/>
</Space>
</Card>

<Flex gap={8} vertical>
<Typography.Text>Payments:</Typography.Text>
<JSONHighlighter json={paymentRequest.data.payments} />
</Flex>
</Flex>
</Card>
</>
);
}
})()}
</SiderLayoutContent>
);
}
14 changes: 14 additions & 0 deletions ui/src/components/payments/PaymentRequests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Space } from "antd";
import { PaymentRequestsTable } from "src/components/payments/PaymentRequestsTable";
import { SiderLayoutContent } from "src/components/shared/SiderLayoutContent";
import { PAYMENT_REQUESTS } from "src/utils/constants";

export function PaymentRequests() {
return (
<SiderLayoutContent description="Description..." title={PAYMENT_REQUESTS}>
<Space direction="vertical" size="large">
<PaymentRequestsTable />
</Space>
</SiderLayoutContent>
);
}
Loading

0 comments on commit 0d29fa1

Please sign in to comment.