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

chore: ui for payment requests #910

Merged
merged 1 commit into from
Jan 28, 2025
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
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
Loading