diff --git a/ui/src/adapters/api/payments.ts b/ui/src/adapters/api/payments.ts new file mode 100644 index 000000000..a7fb7514f --- /dev/null +++ b/ui/src/adapters/api/payments.ts @@ -0,0 +1,210 @@ +import axios from "axios"; +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 { API_VERSION } from "src/utils/constants"; +import { Resource } from "src/utils/types"; + +type PaymentOptionInput = Omit & { + createdAt: string; + modifiedAt: string; +}; + +export const paymentOptionParser = getStrictParser()( + z.object({ + config: z.array( + z.object({ + amount: z.string(), + paymentOptionID: z.number(), + recipient: z.string(), + signingKeyID: z.string(), + }) + ), + createdAt: datetimeParser, + description: z.string(), + id: z.string(), + issuerDID: z.string(), + modifiedAt: datetimeParser, + name: z.string(), + }) +); + +export const paymentConfigurationsParser = getStrictParser()( + z.record( + z.object({ + ChainID: z.number(), + PaymentOption: z.object({ + ContractAddress: z.string(), + Name: z.string(), + Type: z.string(), + }), + PaymentRails: z.string(), + }) + ) +); + +export async function getPaymentOptions({ + env, + identifier, + params: { maxResults, page }, + signal, +}: { + env: Env; + identifier: string; + params: { + maxResults?: number; + page?: number; + }; + signal?: AbortSignal; +}): Promise>> { + try { + const response = await axios({ + baseURL: env.api.url, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "GET", + params: new URLSearchParams({ + ...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}), + ...(page !== undefined ? { page: page.toString() } : {}), + }), + signal, + url: `${API_VERSION}/identities/${identifier}/payment/options`, + }); + return buildSuccessResponse(getResourceParser(paymentOptionParser).parse(response.data)); + } catch (error) { + return buildErrorResponse(error); + } +} + +export type UpsertPaymentOption = Pick; + +export async function createPaymentOption({ + env, + identifier, + payload, +}: { + env: Env; + identifier: string; + payload: UpsertPaymentOption; +}): Promise> { + try { + const response = await axios({ + baseURL: env.api.url, + data: payload, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "POST", + url: `${API_VERSION}/identities/${identifier}/payment/options`, + }); + return buildSuccessResponse(IDParser.parse(response.data)); + } catch (error) { + return buildErrorResponse(error); + } +} + +export async function updatePaymentOption({ + env, + identifier, + payload, + paymentOptionID, +}: { + env: Env; + identifier: string; + payload: UpsertPaymentOption; + paymentOptionID: string; +}) { + try { + await axios({ + baseURL: env.api.url, + data: payload, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "PATCH", + url: `${API_VERSION}/identities/${identifier}/payment/options/${paymentOptionID}`, + }); + + return buildSuccessResponse(undefined); + } catch (error) { + return buildErrorResponse(error); + } +} + +export async function getPaymentOption({ + env, + identifier, + paymentOptionID, + signal, +}: { + env: Env; + identifier: string; + paymentOptionID: string; + signal?: AbortSignal; +}): Promise> { + try { + const response = await axios({ + baseURL: env.api.url, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "GET", + signal, + url: `${API_VERSION}/identities/${identifier}/payment/options/${paymentOptionID}`, + }); + return buildSuccessResponse(paymentOptionParser.parse(response.data)); + } catch (error) { + return buildErrorResponse(error); + } +} + +export async function deletePaymentOption({ + env, + identifier, + paymentOptionID, +}: { + env: Env; + identifier: string; + paymentOptionID: string; +}): Promise> { + try { + const response = await axios({ + baseURL: env.api.url, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "DELETE", + url: `${API_VERSION}/identities/${identifier}/payment/options/${paymentOptionID}`, + }); + return buildSuccessResponse(messageParser.parse(response.data)); + } catch (error) { + return buildErrorResponse(error); + } +} + +export async function getPaymentConfigurations({ + env, + signal, +}: { + env: Env; + signal?: AbortSignal; +}): Promise> { + try { + const response = await axios({ + baseURL: env.api.url, + headers: { + Authorization: buildAuthorizationHeader(env), + }, + method: "GET", + signal, + url: `${API_VERSION}/payment/settings`, + }); + return buildSuccessResponse(paymentConfigurationsParser.parse(response.data)); + } catch (error) { + return buildErrorResponse(error); + } +} diff --git a/ui/src/adapters/parsers/index.ts b/ui/src/adapters/parsers/index.ts index 643d3f42a..05c4e8819 100644 --- a/ui/src/adapters/parsers/index.ts +++ b/ui/src/adapters/parsers/index.ts @@ -48,7 +48,7 @@ export function getListParser( const resourceMetaParser = getStrictParser()( z.object({ - max_results: z.number().int().min(1), + max_results: z.number().int().min(0), page: z.number().int().min(1), total: z.number().int().min(0), }) diff --git a/ui/src/adapters/parsers/view.ts b/ui/src/adapters/parsers/view.ts index 2edea8768..64b5c1c22 100644 --- a/ui/src/adapters/parsers/view.ts +++ b/ui/src/adapters/parsers/view.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { Sorter } from "src/adapters/api"; import { CreateCredential, CreateLink } from "src/adapters/api/credentials"; +import { UpsertPaymentOption } from "src/adapters/api/payments"; import { jsonParser } from "src/adapters/json"; import { getStrictParser } from "src/adapters/parsers"; import { getAttributeValueParser } from "src/adapters/parsers/jsonSchemas"; @@ -299,6 +300,46 @@ export const credentialFormParser = getStrictParser< }) ); +export type PaymentConfigFormData = { + amount: string; + paymentOptionID: string; + recipient: string; + signingKeyID: string; +}; + +export type PaymentOptionFormData = Omit & { + config: Array; +}; + +export const paymentOptionFormParser = getStrictParser< + PaymentOptionFormData, + UpsertPaymentOption +>()( + z + .object({ + config: z.array( + z.object({ + amount: z.string(), + paymentOptionID: z + .string() + .refine((value) => !isNaN(Number(value)), { message: "Must be a valid number" }), + recipient: z.string(), + signingKeyID: z.string(), + }) + ), + description: z.string(), + name: z.string(), + }) + .transform(({ config, description, name }) => ({ + config: config.map(({ paymentOptionID, ...other }) => ({ + ...other, + paymentOptionID: parseInt(paymentOptionID), + })), + description, + name, + })) +); + // Serializers function serializeDate(date: dayjs.Dayjs | Date, format: "date" | "date-time" | "time") { diff --git a/ui/src/assets/icons/display-method.svg b/ui/src/assets/icons/display-method.svg new file mode 100644 index 000000000..98547aadc --- /dev/null +++ b/ui/src/assets/icons/display-method.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/icons/keys.svg b/ui/src/assets/icons/keys.svg new file mode 100644 index 000000000..954bf8179 --- /dev/null +++ b/ui/src/assets/icons/keys.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/assets/icons/payment-options.svg b/ui/src/assets/icons/payment-options.svg new file mode 100644 index 000000000..ef299e4d3 --- /dev/null +++ b/ui/src/assets/icons/payment-options.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/components/connections/ConnectionsTable.tsx b/ui/src/components/connections/ConnectionsTable.tsx index 6a9bbc3ce..07a6d99da 100644 --- a/ui/src/components/connections/ConnectionsTable.tsx +++ b/ui/src/components/connections/ConnectionsTable.tsx @@ -285,14 +285,7 @@ export function ConnectionsTable() { } table={ ({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={connectionsList} locale={{ emptyText: diff --git a/ui/src/components/connections/CredentialsTable.tsx b/ui/src/components/connections/CredentialsTable.tsx index 4d9455441..26affed08 100644 --- a/ui/src/components/connections/CredentialsTable.tsx +++ b/ui/src/components/connections/CredentialsTable.tsx @@ -260,14 +260,7 @@ export function CredentialsTable({ userID }: { userID: string }) { showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={credentialsList} locale={{ emptyText: diff --git a/ui/src/components/credentials/CreateAuthCredential.tsx b/ui/src/components/credentials/CreateAuthCredential.tsx index 9b1b4eb4e..3675d912d 100644 --- a/ui/src/components/credentials/CreateAuthCredential.tsx +++ b/ui/src/components/credentials/CreateAuthCredential.tsx @@ -23,7 +23,7 @@ import { isAsyncTaskStarting, } from "src/utils/async"; import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; -import { VALUE_REQUIRED } from "src/utils/constants"; +import { SAVE, VALUE_REQUIRED } from "src/utils/constants"; export function CreateAuthCredential() { const env = useEnvContext(); @@ -196,7 +196,7 @@ export function CreateAuthCredential() { diff --git a/ui/src/components/credentials/CredentialsTable.tsx b/ui/src/components/credentials/CredentialsTable.tsx index c6c647622..6aaf42afa 100644 --- a/ui/src/components/credentials/CredentialsTable.tsx +++ b/ui/src/components/credentials/CredentialsTable.tsx @@ -368,14 +368,7 @@ export function CredentialsTable() { showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={credentialsList} loading={credentials.status === "reloading"} locale={{ diff --git a/ui/src/components/credentials/LinksTable.tsx b/ui/src/components/credentials/LinksTable.tsx index 28c58ac53..0cf37cb7a 100644 --- a/ui/src/components/credentials/LinksTable.tsx +++ b/ui/src/components/credentials/LinksTable.tsx @@ -364,14 +364,7 @@ export function LinksTable() { showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={linksList} locale={{ emptyText: diff --git a/ui/src/components/display-methods/DisplayMethodDetails.tsx b/ui/src/components/display-methods/DisplayMethodDetails.tsx index 1f13e8fd1..6f9d45d62 100644 --- a/ui/src/components/display-methods/DisplayMethodDetails.tsx +++ b/ui/src/components/display-methods/DisplayMethodDetails.tsx @@ -15,7 +15,12 @@ import { useCallback, useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { z } from "zod"; -import { DISPLAY_METHOD_DETAILS, DISPLAY_METHOD_EDIT, VALUE_REQUIRED } from "../../utils/constants"; +import { + DISPLAY_METHOD_DETAILS, + DISPLAY_METHOD_EDIT, + SAVE, + VALUE_REQUIRED, +} from "../../utils/constants"; import { UpsertDisplayMethod, deleteDisplayMethod, @@ -152,8 +157,8 @@ export function DisplayMethodDetails() { return ; } - const handleEdit = () => { - const { name, url } = form.getFieldsValue(); + const handleEdit = (values: UpsertDisplayMethod) => { + const { name, url } = values; const parsedUrl = z.string().url().safeParse(url); if (parsedUrl.success) { @@ -167,8 +172,10 @@ export function DisplayMethodDetails() { }, }).then((response) => { if (response.success) { - void fetchDisplayMethod(); - setIsEditModalOpen(false); + void fetchDisplayMethod().then(() => { + setIsEditModalOpen(false); + void message.success("Display method edited successfully"); + }); } else { void notifyError(buildAppError(response.error.message)); } @@ -192,7 +199,6 @@ export function DisplayMethodDetails() { const editModal = isAsyncTaskDataAvailable(displayMethod) && ( setIsEditModalOpen(false)} - onSubmit={handleEdit} open={isEditModalOpen} title="Edit display method" > @@ -200,6 +206,7 @@ export function DisplayMethodDetails() { form={form} initialValues={{ name: displayMethod.data.name, url: displayMethod.data.url }} layout="vertical" + onFinish={handleEdit} > @@ -208,6 +215,14 @@ export function DisplayMethodDetails() { + + + + + + ); diff --git a/ui/src/components/display-methods/DisplayMethodForm.tsx b/ui/src/components/display-methods/DisplayMethodForm.tsx index ce0e1e2bc..93788b568 100644 --- a/ui/src/components/display-methods/DisplayMethodForm.tsx +++ b/ui/src/components/display-methods/DisplayMethodForm.tsx @@ -8,7 +8,7 @@ import { DisplayMethodErrorResult } from "src/components/display-methods/Display import { useEnvContext } from "src/contexts/Env"; import { AppError, DisplayMethodMetadata } from "src/domain"; import { AsyncTask, hasAsyncTaskFailed, isAsyncTaskStarting } from "src/utils/async"; -import { VALUE_REQUIRED } from "src/utils/constants"; +import { SAVE, VALUE_REQUIRED } from "src/utils/constants"; export function DisplayMethodForm({ initialValues, @@ -74,7 +74,7 @@ export function DisplayMethodForm({ diff --git a/ui/src/components/display-methods/DisplayMethodsTable.tsx b/ui/src/components/display-methods/DisplayMethodsTable.tsx index 86f52558f..06ea62fc2 100644 --- a/ui/src/components/display-methods/DisplayMethodsTable.tsx +++ b/ui/src/components/display-methods/DisplayMethodsTable.tsx @@ -18,9 +18,9 @@ import { Sorter, parseSorters, serializeSorters } from "src/adapters/api"; import { deleteDisplayMethod, getDisplayMethods } from "src/adapters/api/display-method"; import { notifyErrors, positiveIntegerFromStringParser } from "src/adapters/parsers"; import { tableSorterParser } from "src/adapters/parsers/view"; -import IconIssuers from "src/assets/icons/building-08.svg?react"; import IconCheckMark from "src/assets/icons/check.svg?react"; import IconCopy from "src/assets/icons/copy-01.svg?react"; +import IconDisplayMethod from "src/assets/icons/display-method.svg?react"; import IconDots from "src/assets/icons/dots-vertical.svg?react"; import IconInfoCircle from "src/assets/icons/info-circle.svg?react"; import IconPlus from "src/assets/icons/plus.svg?react"; @@ -259,7 +259,7 @@ export function DisplayMethodsTable() { - } size={48} /> + } size={48} /> No display methods @@ -279,14 +279,7 @@ export function DisplayMethodsTable() { showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={displayMethodsList} locale={{ emptyText: diff --git a/ui/src/components/identities/IdentitiesTable.tsx b/ui/src/components/identities/IdentitiesTable.tsx index 4f62204a1..3bc340bd6 100644 --- a/ui/src/components/identities/IdentitiesTable.tsx +++ b/ui/src/components/identities/IdentitiesTable.tsx @@ -13,10 +13,10 @@ import { } from "antd"; import { useCallback, useEffect, useState } from "react"; import { generatePath, useNavigate, useSearchParams } from "react-router-dom"; -import IconIssuers from "src/assets/icons/building-08.svg?react"; import IconCheckMark from "src/assets/icons/check.svg?react"; import IconCopy from "src/assets/icons/copy-01.svg?react"; import IconDots from "src/assets/icons/dots-vertical.svg?react"; +import IconIssuers from "src/assets/icons/fingerprint-02.svg?react"; import IconInfoCircle from "src/assets/icons/info-circle.svg?react"; import IconPlus from "src/assets/icons/plus.svg?react"; import { ErrorResult } from "src/components/shared/ErrorResult"; @@ -208,14 +208,7 @@ export function IdentitiesTable({ handleAddIdentity }: { handleAddIdentity: () = } table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={filteredIdentifiers} locale={{ emptyText: diff --git a/ui/src/components/identities/Identity.tsx b/ui/src/components/identities/Identity.tsx index 90487a705..304dc1941 100644 --- a/ui/src/components/identities/Identity.tsx +++ b/ui/src/components/identities/Identity.tsx @@ -15,7 +15,7 @@ import { useEnvContext } from "src/contexts/Env"; import { AppError, IdentityDetails } from "src/domain"; import { AsyncTask, hasAsyncTaskFailed, isAsyncTaskStarting } from "src/utils/async"; import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; -import { IDENTITY_DETAILS, VALUE_REQUIRED } from "src/utils/constants"; +import { IDENTITY_DETAILS, SAVE, VALUE_REQUIRED } from "src/utils/constants"; import { formatIdentifier } from "src/utils/forms"; export function Identity() { @@ -64,16 +64,16 @@ export function Identity() { return ; } - const handleEdit = () => { - const { displayName } = form.getFieldsValue(); + const handleEdit = (values: { displayName: string }) => { + const { displayName } = values; void updateIdentityDisplayName({ displayName, env, identifier, }).then((response) => { - setIsEditModalOpen(false); if (response.success) { void fetchIdentity().then(() => { + setIsEditModalOpen(false); void message.success("Identity edited successfully"); }); } else { @@ -161,7 +161,6 @@ export function Identity() { setIsEditModalOpen(false)} - onSubmit={handleEdit} open={isEditModalOpen} title="Edit identity" > @@ -169,6 +168,7 @@ export function Identity() { form={form} initialValues={{ displayName: identity.data.displayName }} layout="vertical" + onFinish={handleEdit} > + + + + + + diff --git a/ui/src/components/identities/IdentityAuthCredentials.tsx b/ui/src/components/identities/IdentityAuthCredentials.tsx index 9647a0766..1e75df9b1 100644 --- a/ui/src/components/identities/IdentityAuthCredentials.tsx +++ b/ui/src/components/identities/IdentityAuthCredentials.tsx @@ -235,14 +235,7 @@ export function IdentityAuthCredentials({ showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={credentialsList} loading={credentials.status === "reloading"} pagination={false} diff --git a/ui/src/components/issuer-state/IssuerState.tsx b/ui/src/components/issuer-state/IssuerState.tsx index f438260bc..d552441ee 100644 --- a/ui/src/components/issuer-state/IssuerState.tsx +++ b/ui/src/components/issuer-state/IssuerState.tsx @@ -323,14 +323,7 @@ export function IssuerState() { } table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={transactionsList} locale={{ emptyText: transactions.status === "failed" && ( diff --git a/ui/src/components/keys/CreateKey.tsx b/ui/src/components/keys/CreateKey.tsx index ff3947b27..23bee6c59 100644 --- a/ui/src/components/keys/CreateKey.tsx +++ b/ui/src/components/keys/CreateKey.tsx @@ -7,7 +7,7 @@ import { useEnvContext } from "src/contexts/Env"; import { useIdentityContext } from "src/contexts/Identity"; import { KeyType } from "src/domain"; import { ROUTES } from "src/routes"; -import { KEY_ADD_NEW, VALUE_REQUIRED } from "src/utils/constants"; +import { KEY_ADD_NEW, SAVE, VALUE_REQUIRED } from "src/utils/constants"; export function CreateKey() { const env = useEnvContext(); @@ -75,7 +75,7 @@ export function CreateKey() { diff --git a/ui/src/components/keys/Key.tsx b/ui/src/components/keys/Key.tsx index b2b61c47a..9155f42b9 100644 --- a/ui/src/components/keys/Key.tsx +++ b/ui/src/components/keys/Key.tsx @@ -1,4 +1,16 @@ -import { App, Button, Card, Dropdown, Flex, Form, Input, Row, Space, Typography } from "antd"; +import { + App, + Button, + Card, + Divider, + Dropdown, + Flex, + Form, + Input, + Row, + Space, + Typography, +} from "antd"; import { useCallback, useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -17,7 +29,7 @@ import { AppError, Key as KeyType } from "src/domain"; import { ROUTES } from "src/routes"; import { AsyncTask, hasAsyncTaskFailed, isAsyncTaskStarting } from "src/utils/async"; import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; -import { KEY_DETAILS, VALUE_REQUIRED } from "src/utils/constants"; +import { KEY_DETAILS, SAVE, VALUE_REQUIRED } from "src/utils/constants"; export function Key() { const env = useEnvContext(); @@ -68,17 +80,17 @@ export function Key() { return ; } - const handleEdit = () => { - const { name } = form.getFieldsValue(); + const handleEdit = (values: UpdateKey) => { + const { name } = values; void updateKeyName({ env, identifier, keyID, payload: { name: name.trim() }, }).then((response) => { - setIsEditModalOpen(false); if (response.success) { void fetchKey().then(() => { + setIsEditModalOpen(false); void message.success("Key edited successfully"); }); } else { @@ -183,11 +195,15 @@ export function Key() { setIsEditModalOpen(false)} - onSubmit={handleEdit} open={isEditModalOpen} title="Edit key" > -
+ + + + + + +
diff --git a/ui/src/components/keys/KeysTable.tsx b/ui/src/components/keys/KeysTable.tsx index a6d8161a9..ef8000309 100644 --- a/ui/src/components/keys/KeysTable.tsx +++ b/ui/src/components/keys/KeysTable.tsx @@ -18,11 +18,11 @@ import { Link, generatePath, useNavigate, useSearchParams } from "react-router-d import { deleteKey, getKeys } from "src/adapters/api/keys"; import { notifyErrors, positiveIntegerFromStringParser } from "src/adapters/parsers"; -import IconIssuers from "src/assets/icons/building-08.svg?react"; import IconCheckMark from "src/assets/icons/check.svg?react"; import IconCopy from "src/assets/icons/copy-01.svg?react"; import IconDots from "src/assets/icons/dots-vertical.svg?react"; import IconInfoCircle from "src/assets/icons/info-circle.svg?react"; +import IconKeys from "src/assets/icons/keys.svg?react"; import IconPlus from "src/assets/icons/plus.svg?react"; import { DeleteItem } from "src/components/schemas/DeleteItem"; import { ErrorResult } from "src/components/shared/ErrorResult"; @@ -254,7 +254,7 @@ export function KeysTable() { - } size={48} /> + } size={48} /> No keys @@ -272,14 +272,7 @@ export function KeysTable() { showDefaultContents={showDefaultContent} table={
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={keysList} locale={{ emptyText: diff --git a/ui/src/components/payments/CreatePaymentOption.tsx b/ui/src/components/payments/CreatePaymentOption.tsx new file mode 100644 index 000000000..74131423b --- /dev/null +++ b/ui/src/components/payments/CreatePaymentOption.tsx @@ -0,0 +1,62 @@ +import { App, Card } from "antd"; +import { useNavigate } from "react-router-dom"; + +import { createPaymentOption } 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 { SiderLayoutContent } from "src/components/shared/SiderLayoutContent"; +import { useEnvContext } from "src/contexts/Env"; +import { useIdentityContext } from "src/contexts/Identity"; +import { ROUTES } from "src/routes"; + +import { PAYMENT_OPTIONS_ADD_NEW } from "src/utils/constants"; + +export function CreatePaymentOption() { + const env = useEnvContext(); + const { identifier } = useIdentityContext(); + + const navigate = useNavigate(); + const { message } = App.useApp(); + + const handleSubmit = (formValues: PaymentOptionFormData) => { + const parsedFormData = paymentOptionFormParser.safeParse(formValues); + + if (parsedFormData.success) { + return void createPaymentOption({ + env, + identifier, + payload: parsedFormData.data, + }).then((response) => { + if (response.success) { + void message.success("Payment option added successfully"); + navigate(ROUTES.paymentOptions.path); + } else { + void message.error(response.error.message); + } + }); + } else { + void notifyParseError(parsedFormData.error); + } + }; + + return ( + + + + + + ); +} diff --git a/ui/src/components/payments/PaymentConfigTable.tsx b/ui/src/components/payments/PaymentConfigTable.tsx new file mode 100644 index 000000000..d85ad4c4e --- /dev/null +++ b/ui/src/components/payments/PaymentConfigTable.tsx @@ -0,0 +1,187 @@ +import { Card, Row, Space, Table, TableColumnsType, Tag, Typography } from "antd"; +import { useCallback, useEffect, useState } from "react"; +import { generatePath, useNavigate } from "react-router-dom"; +import { getKeys } from "src/adapters/api/keys"; +import { getPaymentConfigurations } from "src/adapters/api/payments"; +import { notifyErrors } from "src/adapters/parsers"; +import IconCheckMark from "src/assets/icons/check.svg?react"; +import IconCopy from "src/assets/icons/copy-01.svg?react"; +import { TableCard } from "src/components/shared/TableCard"; +import { useEnvContext } from "src/contexts/Env"; +import { useIdentityContext } from "src/contexts/Identity"; +import { AppError, Key, PaymentConfig, PaymentConfigurations } from "src/domain"; +import { ROUTES } from "src/routes"; +import { AsyncTask, isAsyncTaskDataAvailable, isAsyncTaskStarting } from "src/utils/async"; +import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; + +export function PaymentConfigTable({ configs }: { configs: PaymentConfig[] }) { + const env = useEnvContext(); + const { identifier } = useIdentityContext(); + const navigate = useNavigate(); + + const [keys, setKeys] = useState>({ + status: "pending", + }); + + const [paymentConfigurations, setPaymentConfigurations] = useState< + AsyncTask + >({ + 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] + ); + + const fetchKeys = useCallback( + async (signal?: AbortSignal) => { + setKeys((previousKeys) => + isAsyncTaskDataAvailable(previousKeys) + ? { data: previousKeys.data, status: "reloading" } + : { status: "loading" } + ); + + const response = await getKeys({ + env, + identifier, + params: {}, + signal, + }); + if (response.success) { + setKeys({ + data: response.data.items.successful, + status: "successful", + }); + + void notifyErrors(response.data.items.failed); + } else { + if (!isAbortedError(response.error)) { + setKeys({ error: response.error, status: "failed" }); + } + } + }, + [env, identifier] + ); + + useEffect(() => { + const { aborter } = makeRequestAbortable(fetchKeys); + + return aborter; + }, [fetchKeys]); + + useEffect(() => { + const { aborter } = makeRequestAbortable(fetchPaymentConfigurations); + + return aborter; + }, [fetchPaymentConfigurations]); + + const tableColumns: TableColumnsType = [ + { + dataIndex: "paymentOptionID", + key: "paymentOptionID", + render: (paymentOptionID: PaymentConfig["paymentOptionID"]) => { + const paymentOptionName = + isAsyncTaskDataAvailable(paymentConfigurations) && + paymentConfigurations.data?.[`${paymentOptionID}`]?.PaymentOption.Name; + return {paymentOptionName || paymentOptionID}; + }, + title: "Name", + }, + { + dataIndex: "amount", + key: "amount", + render: (amount: PaymentConfig["amount"]) => {amount}, + title: "Amount", + }, + { + dataIndex: "recipient", + key: "recipient", + render: (recipient: PaymentConfig["recipient"]) => ( + , ], + }} + ellipsis={{ + suffix: recipient.slice(-5), + }} + > + {recipient} + + ), + title: "Recipient", + }, + { + dataIndex: "signingKeyID", + key: "signingKeyID", + render: (signingKeyID: PaymentConfig["signingKeyID"]) => { + const keyName = + isAsyncTaskDataAvailable(keys) && keys.data.find(({ id }) => id === signingKeyID)?.name; + + return ( + + navigate( + generatePath(ROUTES.keyDetails.path, { + keyID: signingKeyID, + }) + ) + } + strong + > + {keyName || signingKeyID} + + ); + }, + title: "Signin key", + }, + ]; + + return ( + No configurations} + isLoading={isAsyncTaskStarting(keys) || isAsyncTaskStarting(paymentConfigurations)} + showDefaultContents={!configs.length} + table={ +
record.paymentOptionID} + showSorterTooltip + sortDirections={["ascend", "descend"]} + /> + } + title={ + + + + + {configs.length} + + + } + /> + ); +} diff --git a/ui/src/components/payments/PaymentOption.tsx b/ui/src/components/payments/PaymentOption.tsx new file mode 100644 index 000000000..3e8426647 --- /dev/null +++ b/ui/src/components/payments/PaymentOption.tsx @@ -0,0 +1,223 @@ +import { App, Button, Card, Dropdown, Flex, Row, Space, Typography } from "antd"; +import { useCallback, useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; + +import { useIdentityContext } from "../../contexts/Identity"; +import { + deletePaymentOption, + getPaymentOption, + updatePaymentOption, +} from "src/adapters/api/payments"; +import { 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"; +import { PaymentConfigTable } from "src/components/payments/PaymentConfigTable"; +import { PaymentOptionForm } from "src/components/payments/PaymentOptionForm"; +import { DeleteItem } from "src/components/schemas/DeleteItem"; +import { Detail } from "src/components/shared/Detail"; +import { EditModal } from "src/components/shared/EditModal"; +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 { ROUTES } from "src/routes"; +import { AsyncTask, hasAsyncTaskFailed, 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"; + +export function PaymentOption() { + const env = useEnvContext(); + const { identifier } = useIdentityContext(); + const { message } = App.useApp(); + const navigate = useNavigate(); + + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [paymentOption, setPaymentOption] = useState>({ + status: "pending", + }); + + const { paymentOptionID } = useParams(); + + const fetchPaymentOption = useCallback( + async (signal?: AbortSignal) => { + if (paymentOptionID) { + setPaymentOption({ status: "loading" }); + + const response = await getPaymentOption({ + env, + identifier, + paymentOptionID, + signal, + }); + + if (response.success) { + setPaymentOption({ data: response.data, status: "successful" }); + } else { + if (!isAbortedError(response.error)) { + setPaymentOption({ error: response.error, status: "failed" }); + } + } + } + }, + [env, paymentOptionID, identifier] + ); + + useEffect(() => { + const { aborter } = makeRequestAbortable(fetchPaymentOption); + + return aborter; + }, [fetchPaymentOption]); + + if (!paymentOptionID) { + return ; + } + + const handleDeletePaymentOption = () => { + void deletePaymentOption({ env, identifier, paymentOptionID }).then((response) => { + if (response.success) { + navigate(ROUTES.paymentOptions.path); + void message.success(response.data.message); + } else { + void message.error(response.error.message); + } + }); + }; + + const handleEdit = (formValues: PaymentOptionFormData) => { + const parsedFormData = paymentOptionFormParser.safeParse(formValues); + + if (parsedFormData.success) { + return void updatePaymentOption({ + env, + identifier, + payload: parsedFormData.data, + paymentOptionID, + }).then((response) => { + if (response.success) { + void fetchPaymentOption().then(() => { + setIsEditModalOpen(false); + void message.success("Payment option edited successfully"); + }); + } else { + void message.error(response.error.message); + } + }); + } else { + void notifyParseError(parsedFormData.error); + } + }; + + return ( + + {(() => { + if (hasAsyncTaskFailed(paymentOption)) { + return ( + + + + ); + } else if (isAsyncTaskStarting(paymentOption)) { + return ( + + + + ); + } else { + return ( + <> + + + {paymentOption.data.name} + + + + + + + + + + + + + + setShowConfigForm(false)} + onSubmit={handleAddConfig} + open={showConfigForm} + paymentConfigurations={paymentConfigurations.data} + /> + + ); + } + })()} + + ); +} diff --git a/ui/src/components/payments/PaymentOptions.tsx b/ui/src/components/payments/PaymentOptions.tsx new file mode 100644 index 000000000..ecc7af885 --- /dev/null +++ b/ui/src/components/payments/PaymentOptions.tsx @@ -0,0 +1,32 @@ +import { Button, Space } from "antd"; +import { generatePath, useNavigate } from "react-router-dom"; + +import IconPlus from "src/assets/icons/plus.svg?react"; +import { PaymentOptionsTable } from "src/components/payments/PaymentOptionsTable"; +import { SiderLayoutContent } from "src/components/shared/SiderLayoutContent"; +import { ROUTES } from "src/routes"; +import { PAYMENT_OPTIONS, PAYMENT_OPTIONS_ADD } from "src/utils/constants"; + +export function PaymentOptions() { + const navigate = useNavigate(); + + return ( + } + onClick={() => navigate(generatePath(ROUTES.createPaymentOption.path))} + type="primary" + > + {PAYMENT_OPTIONS_ADD} + + } + title={PAYMENT_OPTIONS} + > + + + + + ); +} diff --git a/ui/src/components/payments/PaymentOptionsTable.tsx b/ui/src/components/payments/PaymentOptionsTable.tsx new file mode 100644 index 000000000..604183e32 --- /dev/null +++ b/ui/src/components/payments/PaymentOptionsTable.tsx @@ -0,0 +1,305 @@ +import { + App, + Avatar, + Button, + Card, + Dropdown, + Row, + Space, + Table, + TableColumnsType, + Tag, + Tooltip, + Typography, +} from "antd"; +import { ItemType } from "antd/es/menu/interface"; +import { useCallback, useEffect, useState } from "react"; +import { Link, generatePath, useNavigate, useSearchParams } from "react-router-dom"; + +import { deletePaymentOption, getPaymentOptions } from "src/adapters/api/payments"; +import { notifyErrors, positiveIntegerFromStringParser } from "src/adapters/parsers"; +import IconDots from "src/assets/icons/dots-vertical.svg?react"; +import IconInfoCircle from "src/assets/icons/info-circle.svg?react"; +import IconPaymentOptions from "src/assets/icons/payment-options.svg?react"; +import IconPlus from "src/assets/icons/plus.svg?react"; +import { DeleteItem } from "src/components/schemas/DeleteItem"; +import { ErrorResult } from "src/components/shared/ErrorResult"; +import { NoResults } from "src/components/shared/NoResults"; +import { TableCard } from "src/components/shared/TableCard"; +import { useEnvContext } from "src/contexts/Env"; +import { useIdentityContext } from "src/contexts/Identity"; +import { AppError, PaymentOption } from "src/domain"; +import { ROUTES } from "src/routes"; +import { AsyncTask, isAsyncTaskDataAvailable, isAsyncTaskStarting } from "src/utils/async"; +import { isAbortedError, makeRequestAbortable } from "src/utils/browser"; +import { + DEFAULT_PAGINATION_MAX_RESULTS, + DEFAULT_PAGINATION_PAGE, + DEFAULT_PAGINATION_TOTAL, + DETAILS, + DOTS_DROPDOWN_WIDTH, + PAGINATION_MAX_RESULTS_PARAM, + PAGINATION_PAGE_PARAM, + PAYMENT_OPTIONS_ADD_NEW, + QUERY_SEARCH_PARAM, +} from "src/utils/constants"; +import { formatDate } from "src/utils/forms"; + +export function PaymentOptionsTable() { + const env = useEnvContext(); + const { identifier } = useIdentityContext(); + const { message } = App.useApp(); + const navigate = useNavigate(); + + const [paymentOptions, setPaymentOptions] = useState>({ + status: "pending", + }); + + const [searchParams, setSearchParams] = useSearchParams(); + const queryParam = searchParams.get(QUERY_SEARCH_PARAM); + const paginationPageParam = searchParams.get(PAGINATION_PAGE_PARAM); + const paginationMaxResultsParam = searchParams.get(PAGINATION_MAX_RESULTS_PARAM); + + const paginationPageParsed = positiveIntegerFromStringParser.safeParse(paginationPageParam); + const paginationMaxResultsParsed = + positiveIntegerFromStringParser.safeParse(paginationMaxResultsParam); + + const [paginationTotal, setPaginationTotal] = useState(DEFAULT_PAGINATION_TOTAL); + const paginationPage = paginationPageParsed.success + ? paginationPageParsed.data + : DEFAULT_PAGINATION_PAGE; + const paginationMaxResults = paginationMaxResultsParsed.success + ? paginationMaxResultsParsed.data + : DEFAULT_PAGINATION_MAX_RESULTS; + + const paymentOptionsList = isAsyncTaskDataAvailable(paymentOptions) ? paymentOptions.data : []; + const showDefaultContent = + paymentOptions.status === "successful" && paymentOptionsList.length === 0; + + const tableColumns: TableColumnsType = [ + { + dataIndex: "name", + key: "name", + render: (name: PaymentOption["name"], { description, id }: PaymentOption) => ( + + navigate( + generatePath(ROUTES.paymentOptionDetails.path, { + paymentOptionID: id, + }) + ) + } + strong + > + {name} + + ), + title: "Name", + }, + { + dataIndex: "createdAt", + key: "createdAt", + render: (createdAt: PaymentOption["createdAt"]) => ( + {formatDate(createdAt)} + ), + sorter: ({ createdAt: a }, { createdAt: b }) => b.getTime() - a.getTime(), + title: "Created date", + }, + { + dataIndex: "modifiedAt", + key: "modifiedAt", + render: (modifiedAt: PaymentOption["modifiedAt"]) => ( + {formatDate(modifiedAt)} + ), + sorter: ({ modifiedAt: a }, { modifiedAt: b }) => b.getTime() - a.getTime(), + title: "Modified date", + }, + + { + dataIndex: "id", + key: "id", + render: (id: PaymentOption["id"]) => { + const items: Array = [ + { + icon: , + key: "details", + label: DETAILS, + onClick: () => + navigate( + generatePath(ROUTES.paymentOptionDetails.path, { + paymentOptionID: id, + }) + ), + }, + { + key: "divider1", + type: "divider", + }, + { + danger: true, + key: "delete", + label: ( + handleDeletePaymentOption(id)} + title="Are you sure you want to delete this payment option?" + /> + ), + }, + ]; + + return ( + + + + + + ); + }, + + width: DOTS_DROPDOWN_WIDTH, + }, + ]; + + const updateUrlParams = useCallback( + ({ maxResults, page }: { maxResults?: number; page?: number }) => { + setSearchParams((previousParams) => { + const params = new URLSearchParams(previousParams); + params.set( + PAGINATION_PAGE_PARAM, + page !== undefined ? page.toString() : DEFAULT_PAGINATION_PAGE.toString() + ); + params.set( + PAGINATION_MAX_RESULTS_PARAM, + maxResults !== undefined + ? maxResults.toString() + : DEFAULT_PAGINATION_MAX_RESULTS.toString() + ); + + return params; + }); + }, + [setSearchParams] + ); + + const fetchPaymentOptions = useCallback( + async (signal?: AbortSignal) => { + setPaymentOptions((previousPaymentOptions) => + isAsyncTaskDataAvailable(previousPaymentOptions) + ? { data: previousPaymentOptions.data, status: "reloading" } + : { status: "loading" } + ); + + const response = await getPaymentOptions({ + env, + identifier, + params: { + maxResults: paginationMaxResults, + page: paginationPage, + }, + signal, + }); + if (response.success) { + setPaymentOptions({ + data: response.data.items.successful, + status: "successful", + }); + setPaginationTotal(response.data.meta.total); + updateUrlParams({ + maxResults: response.data.meta.max_results, + page: response.data.meta.page, + }); + void notifyErrors(response.data.items.failed); + } else { + if (!isAbortedError(response.error)) { + setPaymentOptions({ error: response.error, status: "failed" }); + } + } + }, + [env, paginationMaxResults, paginationPage, identifier, updateUrlParams] + ); + + const handleDeletePaymentOption = (paymentOptionID: string) => { + void deletePaymentOption({ env, identifier, paymentOptionID }).then((response) => { + if (response.success) { + void fetchPaymentOptions(); + void message.success(response.data.message); + } else { + void message.error(response.error.message); + } + }); + }; + + useEffect(() => { + const { aborter } = makeRequestAbortable(fetchPaymentOptions); + + return aborter; + }, [fetchPaymentOptions]); + + return ( + + } size={48} /> + + No payment options + + + Your payment options will be listed here. + + + + + + + } + isLoading={isAsyncTaskStarting(paymentOptions)} + query={queryParam} + showDefaultContents={showDefaultContent} + table={ +
+ ) : ( + + ), + }} + onChange={({ current, pageSize, total }) => { + setPaginationTotal(total || DEFAULT_PAGINATION_TOTAL); + updateUrlParams({ + maxResults: pageSize, + page: current, + }); + }} + pagination={{ + current: paginationPage, + hideOnSinglePage: true, + pageSize: paginationMaxResults, + position: ["bottomRight"], + total: paginationTotal, + }} + rowKey="id" + showSorterTooltip + sortDirections={["ascend", "descend"]} + /> + } + title={ + + + + {paginationTotal} + + + } + /> + ); +} diff --git a/ui/src/components/schemas/SchemaViewer.tsx b/ui/src/components/schemas/SchemaViewer.tsx index 539818bdb..a8adcee1d 100644 --- a/ui/src/components/schemas/SchemaViewer.tsx +++ b/ui/src/components/schemas/SchemaViewer.tsx @@ -55,7 +55,7 @@ export function SchemaViewer({ {contents} - {displayMethods?.length && ( + {!!displayMethods?.length && (
({ - title: ( - - <>{title} - - ), - ...column, - }))} + columns={tableColumns} dataSource={schemaList} locale={{ emptyText: diff --git a/ui/src/components/shared/EditModal.tsx b/ui/src/components/shared/EditModal.tsx index 2d0f106e6..f2f8a4e6b 100644 --- a/ui/src/components/shared/EditModal.tsx +++ b/ui/src/components/shared/EditModal.tsx @@ -1,38 +1,33 @@ import { Divider, Modal } from "antd"; import { ReactNode } from "react"; import IconClose from "src/assets/icons/x.svg?react"; -import { CLOSE, SAVE } from "src/utils/constants"; export function EditModal({ children, onClose, - onSubmit, open, title, }: { children: ReactNode; onClose: () => void; - onSubmit: () => void; open: boolean; title: string; }) { return ( } + destroyOnClose + footer={null} maskClosable - okText={SAVE} onCancel={onClose} - onOk={onSubmit} open={open} style={{ maxWidth: 600 }} title={title} > {children} - ); } diff --git a/ui/src/components/shared/Router.tsx b/ui/src/components/shared/Router.tsx index be894e50a..5a57bdbdb 100644 --- a/ui/src/components/shared/Router.tsx +++ b/ui/src/components/shared/Router.tsx @@ -21,6 +21,9 @@ import { Key } from "src/components/keys/Key"; import { Keys } from "src/components/keys/Keys"; import { FullWidthLayout } from "src/components/layouts/FullWidthLayout"; import { SiderLayout } from "src/components/layouts/SiderLayout"; +import { CreatePaymentOption } from "src/components/payments/CreatePaymentOption"; +import { PaymentOption } from "src/components/payments/PaymentOption"; +import { PaymentOptions } from "src/components/payments/PaymentOptions"; import { ImportSchema } from "src/components/schemas/ImportSchema"; import { SchemaDetails } from "src/components/schemas/SchemaDetails"; import { Schemas } from "src/components/schemas/Schemas"; @@ -32,10 +35,11 @@ import { ROOT_PATH } from "src/utils/constants"; const COMPONENTS: Record = { connectionDetails: ConnectionDetails, connections: ConnectionsTable, - createAuthCredential: CreateAuthCredential, + createAuthCredential: CreateAuthCredential, createDisplayMethod: CreateDisplayMethod, createIdentity: CreateIdentity, createKey: CreateKey, + createPaymentOption: CreatePaymentOption, credentialDetails: CredentialDetails, credentials: Credentials, displayMethodDetails: DisplayMethodDetails, @@ -50,6 +54,8 @@ const COMPONENTS: Record = { linkDetails: LinkDetails, notFound: NotFound, onboarding: Onboarding, + paymentOptionDetails: PaymentOption, + paymentOptions: PaymentOptions, schemaDetails: SchemaDetails, schemas: Schemas, }; diff --git a/ui/src/components/shared/SiderMenu.tsx b/ui/src/components/shared/SiderMenu.tsx index 4c88977d8..1b117319b 100644 --- a/ui/src/components/shared/SiderMenu.tsx +++ b/ui/src/components/shared/SiderMenu.tsx @@ -3,10 +3,13 @@ import { useState } from "react"; import { generatePath, matchRoutes, useLocation, useNavigate } from "react-router-dom"; import IconCredentials from "src/assets/icons/credit-card-refresh.svg?react"; +import IconDisplayMethod from "src/assets/icons/display-method.svg?react"; import IconFile from "src/assets/icons/file-05.svg?react"; import IconSchema from "src/assets/icons/file-search-02.svg?react"; import IconIdentities from "src/assets/icons/fingerprint-02.svg?react"; +import IconKeys from "src/assets/icons/keys.svg?react"; import IconLink from "src/assets/icons/link-external-01.svg?react"; +import IconPaymentOptions from "src/assets/icons/payment-options.svg?react"; import IconSettings from "src/assets/icons/settings-01.svg?react"; import IconIssuerState from "src/assets/icons/switch-horizontal.svg?react"; import IconConnections from "src/assets/icons/users-01.svg?react"; @@ -26,6 +29,7 @@ import { IDENTITIES, ISSUER_STATE, KEYS, + PAYMENT_OPTIONS, SCHEMAS, } from "src/utils/constants"; @@ -52,6 +56,7 @@ export function SiderMenu({ const identitiesPath = ROUTES.identities.path; const displayMethodsPath = ROUTES.displayMethods.path; const keysPath = ROUTES.keys.path; + const paymentOptionsPath = ROUTES.paymentOptions.path; const getSelectedKey = (): string[] => { if ( @@ -112,6 +117,17 @@ export function SiderMenu({ ) ) { return [keysPath]; + } else if ( + matchRoutes( + [ + { path: paymentOptionsPath }, + { path: ROUTES.createPaymentOption.path }, + { path: ROUTES.paymentOptionDetails.path }, + ], + pathname + ) + ) { + return [paymentOptionsPath]; } return []; @@ -144,7 +160,7 @@ export function SiderMenu({ title: "", }, { - icon: , + icon: , key: displayMethodsPath, label: DISPLAY_METHODS, onClick: () => onMenuClick(displayMethodsPath), @@ -186,12 +202,19 @@ export function SiderMenu({ }, { - icon: , + icon: , key: keysPath, label: KEYS, onClick: () => onMenuClick(keysPath), title: "", }, + { + icon: , + key: paymentOptionsPath, + label: PAYMENT_OPTIONS, + onClick: () => onMenuClick(paymentOptionsPath), + title: "", + }, ]} selectedKeys={getSelectedKey()} style={{ marginTop: 16 }} diff --git a/ui/src/domain/index.ts b/ui/src/domain/index.ts index 5be033699..e8cbed721 100644 --- a/ui/src/domain/index.ts +++ b/ui/src/domain/index.ts @@ -70,3 +70,10 @@ export { DisplayMethodType } from "./display-method"; export type { Key } from "src/domain/key"; export { KeyType } from "src/domain/key"; + +export type { + PaymentOption, + PaymentConfiguration, + PaymentConfigurations, + PaymentConfig, +} from "src/domain/payment"; diff --git a/ui/src/domain/payment.ts b/ui/src/domain/payment.ts new file mode 100644 index 000000000..4b716e6dd --- /dev/null +++ b/ui/src/domain/payment.ts @@ -0,0 +1,30 @@ +export type PaymentConfig = { + amount: string; + paymentOptionID: number; + recipient: string; + signingKeyID: string; +}; + +export type PaymentOption = { + config: Array; + createdAt: Date; + description: string; + id: string; + issuerDID: string; + modifiedAt: Date; + name: string; +}; + +export type PaymentConfiguration = { + ChainID: number; + PaymentOption: { + ContractAddress: string; + Name: string; + Type: string; + }; + PaymentRails: string; +}; + +export type PaymentConfigurations = { + [key: string]: PaymentConfiguration; +}; diff --git a/ui/src/routes.ts b/ui/src/routes.ts index af716b8b3..2483c8fd6 100644 --- a/ui/src/routes.ts +++ b/ui/src/routes.ts @@ -20,7 +20,10 @@ export type RouteID = | "createDisplayMethod" | "keys" | "keyDetails" - | "createKey"; + | "createKey" + | "createPaymentOption" + | "paymentOptions" + | "paymentOptionDetails"; export type Layout = "fullWidth" | "fullWidthGrey" | "sider"; @@ -57,6 +60,10 @@ export const ROUTES: Routes = { layout: "sider", path: "/keys/create", }, + createPaymentOption: { + layout: "sider", + path: "/payments/create", + }, credentialDetails: { layout: "sider", path: "/credentials/issued/:credentialID", @@ -113,6 +120,14 @@ export const ROUTES: Routes = { layout: "fullWidthGrey", path: "/onboarding", }, + paymentOptionDetails: { + layout: "sider", + path: "/payments/:paymentOptionID", + }, + paymentOptions: { + layout: "sider", + path: "/payments", + }, schemaDetails: { layout: "sider", path: "/schemas/:schemaID", diff --git a/ui/src/styles/index.scss b/ui/src/styles/index.scss index c23895457..26441e831 100644 --- a/ui/src/styles/index.scss +++ b/ui/src/styles/index.scss @@ -351,6 +351,12 @@ overflow: auto; } +.ant-table-content { + .ant-table-thead > tr > th { + color: $text-color-secondary; + } +} + /* CUSTOM */ .background-grey { @@ -403,6 +409,7 @@ .menu-sider-layout { height: 100%; flex-flow: column; + min-height: fit-content; .ant-menu-title-content { width: 100%; diff --git a/ui/src/utils/constants.ts b/ui/src/utils/constants.ts index 55b8c380d..f8b494e12 100644 --- a/ui/src/utils/constants.ts +++ b/ui/src/utils/constants.ts @@ -33,6 +33,10 @@ export const KEYS = "Keys"; export const KEY_ADD = "Add key"; export const KEY_ADD_NEW = "Add new key"; export const KEY_DETAILS = "Key details"; +export const PAYMENT_OPTIONS = "Payment options"; +export const PAYMENT_OPTIONS_ADD = "Add payment option"; +export const PAYMENT_OPTIONS_ADD_NEW = "Add new payment option"; +export const PAYMENT_OPTIONS_DETAILS = "Payment option details"; export const LINKS = "Links"; export const REVOCATION = "Revocation"; export const REVOKE = "Revoke";