From 3a215d0b832a37e5e2f1e7c1eefea055a77508aa Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 11 Oct 2023 14:27:20 -0400 Subject: [PATCH 1/5] SDP-782: State refactor: payments (#22) * State refactor: payments * Cleanup --- src/apiQueries/usePayments.ts | 24 ++++++ src/components/Pagination/index.tsx | 41 +++++---- src/components/PaymentsTable.tsx | 10 +-- src/components/Table/index.tsx | 12 ++- src/components/Table/styles.scss | 23 +++++- src/pages/DisbursementDetails.tsx | 35 ++------ src/pages/Disbursements.tsx | 37 ++------- src/pages/Payments.tsx | 108 ++++++------------------ src/pages/ReceiverDetails.tsx | 50 +++-------- src/pages/Receivers.tsx | 35 ++------ src/store/ducks/payments.ts | 124 ---------------------------- src/store/index.ts | 2 - src/types/index.ts | 9 -- 13 files changed, 141 insertions(+), 369 deletions(-) create mode 100644 src/apiQueries/usePayments.ts delete mode 100644 src/store/ducks/payments.ts diff --git a/src/apiQueries/usePayments.ts b/src/apiQueries/usePayments.ts new file mode 100644 index 0000000..aa91f43 --- /dev/null +++ b/src/apiQueries/usePayments.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { handleSearchParams } from "api/handleSearchParams"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiPayments, AppError, PaymentsSearchParams } from "types"; + +export const usePayments = (searchParams?: PaymentsSearchParams) => { + // ALL status is for UI only + if (searchParams?.status === "ALL") { + delete searchParams.status; + } + + const params = handleSearchParams(searchParams); + + const query = useQuery({ + queryKey: ["payments", { ...searchParams }], + queryFn: async () => { + return await fetchApi(`${API_URL}/payments/${params}`); + }, + keepPreviousData: true, + }); + + return query; +}; diff --git a/src/components/Pagination/index.tsx b/src/components/Pagination/index.tsx index 10744c7..abfcdd1 100644 --- a/src/components/Pagination/index.tsx +++ b/src/components/Pagination/index.tsx @@ -1,26 +1,27 @@ +import { useState } from "react"; import { Button, Icon, Input } from "@stellar/design-system"; import "./styles.scss"; interface PaginationProps { currentPage: number; maxPages: number; - onChange: (event: React.ChangeEvent) => void; - onBlur: (currentPage: number) => void; - onPrevious: (event: React.MouseEvent) => void; - onNext: (event: React.MouseEvent) => void; + onSetPage: (page: number) => void; isLoading: boolean; } export const Pagination = ({ currentPage, maxPages, - onChange, - onBlur, - onPrevious, - onNext, + onSetPage, isLoading, }: PaginationProps) => { - const isError = currentPage > maxPages; + const [page, setPage] = useState(); + const isError = (page || 0) > maxPages; + + const handleChange = (event: React.ChangeEvent) => { + event.preventDefault(); + setPage(Number(event.target.value)); + }; const handleBlur = ( event: @@ -29,11 +30,21 @@ export const Pagination = ({ ) => { event.preventDefault(); - if (!isError) { - onBlur(currentPage); + if (!isError && page) { + onSetPage(page); + setPage(undefined); } }; + const handlePageChange = ( + event: React.MouseEvent, + direction: "prev" | "next", + ) => { + event.preventDefault(); + const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1; + onSetPage(newPage); + }; + return (
Page @@ -41,8 +52,8 @@ export const Pagination = ({ } - onClick={onPrevious} + onClick={(event) => handlePageChange(event, "prev")} disabled={isError || isLoading || currentPage === 1} />
diff --git a/src/components/PaymentsTable.tsx b/src/components/PaymentsTable.tsx index 321fd38..444e0e2 100644 --- a/src/components/PaymentsTable.tsx +++ b/src/components/PaymentsTable.tsx @@ -6,20 +6,20 @@ import { AssetAmount } from "components/AssetAmount"; import { PaymentStatus } from "components/PaymentStatus"; import { Table } from "components/Table"; import { formatDateTime } from "helpers/formatIntlDateTime"; -import { ApiPayment, ActionStatus } from "types"; +import { ApiPayment } from "types"; interface PaymentsTableProps { paymentItems: ApiPayment[]; apiError: string | boolean | undefined; isFiltersSelected: boolean | undefined; - status: ActionStatus | undefined; + isLoading: boolean; } export const PaymentsTable = ({ paymentItems, apiError, isFiltersSelected, - status, + isLoading, }: PaymentsTableProps) => { const navigate = useNavigate(); @@ -48,7 +48,7 @@ export const PaymentsTable = ({ } if (paymentItems?.length === 0) { - if (status === "PENDING") { + if (isLoading) { return
Loading…
; } @@ -66,7 +66,7 @@ export const PaymentsTable = ({ return (
- +
{/* TODO: put back once ready */} {/* diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index b744f57..016b317 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -1,4 +1,4 @@ -import { Icon } from "@stellar/design-system"; +import { Icon, Loader } from "@stellar/design-system"; import { SortDirection } from "types"; import "./styles.scss"; @@ -140,16 +140,24 @@ interface TableComponent { interface TableProps extends React.HtmlHTMLAttributes { children: JSX.Element[]; + isLoading?: boolean; } export const Table: React.FC & TableComponent = ({ children, + isLoading, }: TableProps) => { return ( -
+
{children}
+ {isLoading ? : null} ); }; diff --git a/src/components/Table/styles.scss b/src/components/Table/styles.scss index 5490939..699e174 100644 --- a/src/components/Table/styles.scss +++ b/src/components/Table/styles.scss @@ -3,11 +3,32 @@ .Table-v2__container { width: 100%; position: relative; + + &--loading { + overflow-y: hidden; + pointer-events: none; + + .Table-v2__wrapper { + opacity: var(--opacity-disabled-button); + } + + .Loader { + --Loader-color: var(--color-gray-60); + --Loader-size: 2rem; + + position: absolute; + top: 2rem; + left: 50%; + transform: translate(-50%, -50%); + } + } } .Table-v2__wrapper { overflow-x: auto; overflow-y: hidden; + opacity: 1; + transition: opacity var(--anim-transition-default); } table.Table-v2 { @@ -17,7 +38,7 @@ table.Table-v2 { thead tr, tr:not(:last-child) { border-bottom: 1px solid var(--color-gray-30); - transition: background-color linear var(--anim-transition-default); + transition: background-color var(--anim-transition-default); &.Table-v2__row--highlighted { background-color: var(--color-gray-10); diff --git a/src/pages/DisbursementDetails.tsx b/src/pages/DisbursementDetails.tsx index 660cfef..69d4c37 100644 --- a/src/pages/DisbursementDetails.tsx +++ b/src/pages/DisbursementDetails.tsx @@ -128,30 +128,6 @@ export const DisbursementDetails = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch(getDisbursementReceiversAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const goToReceiver = ( event: React.MouseEvent, receiverId: string, @@ -511,13 +487,12 @@ export const DisbursementDetails = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getDisbursementReceiversAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={disbursementDetails.status === "PENDING"} /> diff --git a/src/pages/Disbursements.tsx b/src/pages/Disbursements.tsx index 7b85b04..8079e2a 100644 --- a/src/pages/Disbursements.tsx +++ b/src/pages/Disbursements.tsx @@ -164,32 +164,6 @@ export const Disbursements = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch( - getDisbursementsWithParamsAction({ page: currentPage.toString() }), - ); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - return ( <> @@ -304,13 +278,12 @@ export const Disbursements = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getDisbursementsWithParamsAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={disbursements.status === "PENDING"} /> diff --git a/src/pages/Payments.tsx b/src/pages/Payments.tsx index 7986a35..c7bda8a 100644 --- a/src/pages/Payments.tsx +++ b/src/pages/Payments.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Button, Heading, Icon, Input, Select } from "@stellar/design-system"; -import { useDispatch } from "react-redux"; import { FilterMenu } from "components/FilterMenu"; import { Pagination } from "components/Pagination"; @@ -8,20 +7,12 @@ import { PaymentsTable } from "components/PaymentsTable"; import { SearchInput } from "components/SearchInput"; import { SectionHeader } from "components/SectionHeader"; +import { usePayments } from "apiQueries/usePayments"; import { PAGE_LIMIT_OPTIONS } from "constants/settings"; import { number } from "helpers/formatIntlNumber"; -import { useRedux } from "hooks/useRedux"; -import { AppDispatch } from "store"; -import { - getPaymentsWithParamsAction, - getPaymentsAction, -} from "store/ducks/payments"; import { CommonFilters } from "types"; export const Payments = () => { - const { payments } = useRedux("payments"); - - // TODO: handle search in progress const [isSearchInProgress] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [pageLimit, setPageLimit] = useState(20); @@ -33,18 +24,25 @@ export const Payments = () => { }; const [filters, setFilters] = useState(initFilters); + // Using extra param to trigger API call when we want, not on every filter + // state change + const [queryFilters, setQueryFilters] = useState({}); + + const { + data: payments, + error, + isLoading, + isFetching, + } = usePayments({ + page: currentPage.toString(), + page_limit: pageLimit.toString(), + ...queryFilters, + }); const isFiltersSelected = Object.values(filters).filter((v) => Boolean(v)).length > 0; - const dispatch: AppDispatch = useDispatch(); - - useEffect(() => { - dispatch(getPaymentsAction()); - }, [dispatch]); - - const apiError = payments.status === "ERROR" && payments.errorString; - const maxPages = payments.pagination?.pages || 1; + const maxPages = payments?.pagination?.pages || 1; const handleSearchSubmit = () => { alert("TODO: search submit"); @@ -64,26 +62,14 @@ export const Payments = () => { }; const handleFilterSubmit = () => { - dispatch( - getPaymentsWithParamsAction({ - page: "1", - ...filters, - }), - ); - setCurrentPage(1); + setQueryFilters(filters); }; const handleFilterReset = () => { - dispatch( - getPaymentsWithParamsAction({ - page: "1", - ...initFilters, - }), - ); - - setFilters(initFilters); setCurrentPage(1); + setFilters(initFilters); + setQueryFilters(initFilters); }; const handleExport = ( @@ -97,42 +83,8 @@ export const Payments = () => { event: React.ChangeEvent, ) => { event.preventDefault(); - - const pageLimit = Number(event.target.value); - setPageLimit(pageLimit); setCurrentPage(1); - - // Need to make sure we'll be loading page 1 - dispatch( - getPaymentsWithParamsAction({ - page_limit: pageLimit.toString(), - page: "1", - }), - ); - }; - - const handlePageChange = (currentPage: number) => { - dispatch(getPaymentsWithParamsAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); + setPageLimit(Number(event.target.value)); }; return ( @@ -141,7 +93,7 @@ export const Payments = () => { - {payments.pagination?.total && payments.pagination.total > 0 + {payments?.pagination?.total && payments.pagination.total > 0 ? `${number.format(payments.pagination.total)} ` : ""} Payments @@ -238,24 +190,18 @@ export const Payments = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); - }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} - isLoading={payments.status === "PENDING"} + onSetPage={(page) => setCurrentPage(page)} + isLoading={isLoading || isFetching} /> ); diff --git a/src/pages/ReceiverDetails.tsx b/src/pages/ReceiverDetails.tsx index fa8fd88..b545cf9 100644 --- a/src/pages/ReceiverDetails.tsx +++ b/src/pages/ReceiverDetails.tsx @@ -99,37 +99,6 @@ export const ReceiverDetails = () => { } }; - const handlePageChange = (currentPage: number) => { - if (receiverId) { - dispatch( - getReceiverPaymentsWithParamsAction({ - receiver_id: receiverId, - page: currentPage.toString(), - }), - ); - } - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const handleRetryInvitation = (receiverWalletId: string) => { dispatch(retryInvitationSMSAction({ receiverWalletId })); }; @@ -570,13 +539,18 @@ export const ReceiverDetails = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + + if (receiverId) { + dispatch( + getReceiverPaymentsWithParamsAction({ + receiver_id: receiverId, + page: page.toString(), + }), + ); + } }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={receiverPayments.status === "PENDING"} /> @@ -587,7 +561,7 @@ export const ReceiverDetails = () => { paymentItems={receiverPayments.items} apiError={receiverPayments.errorString} isFiltersSelected={undefined} - status={receiverPayments.status} + isLoading={receiverPayments.status === "PENDING"} /> diff --git a/src/pages/Receivers.tsx b/src/pages/Receivers.tsx index b6244f4..d343299 100644 --- a/src/pages/Receivers.tsx +++ b/src/pages/Receivers.tsx @@ -140,30 +140,6 @@ export const Receivers = () => { ); }; - const handlePageChange = (currentPage: number) => { - dispatch(getReceiversWithParamsAction({ page: currentPage.toString() })); - }; - - const handleNextPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage + 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - - const handlePrevPage = ( - event: React.MouseEvent, - ) => { - event.preventDefault(); - const newPage = currentPage - 1; - - setCurrentPage(newPage); - handlePageChange(newPage); - }; - const handleReceiverClicked = ( event: React.MouseEvent, receiverId: string, @@ -273,13 +249,12 @@ export const Receivers = () => { { - event.preventDefault(); - setCurrentPage(Number(event.target.value)); + onSetPage={(page) => { + setCurrentPage(page); + dispatch( + getReceiversWithParamsAction({ page: page.toString() }), + ); }} - onBlur={handlePageChange} - onNext={handleNextPage} - onPrevious={handlePrevPage} isLoading={receivers.status === "PENDING"} /> diff --git a/src/store/ducks/payments.ts b/src/store/ducks/payments.ts deleted file mode 100644 index eb14605..0000000 --- a/src/store/ducks/payments.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { RootState } from "store"; -import { getPayments } from "api/getPayments"; -import { handleApiErrorString } from "api/handleApiErrorString"; -import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; -import { refreshSessionToken } from "helpers/refreshSessionToken"; -import { - ApiError, - ApiPayments, - PaymentsInitialState, - PaymentsSearchParams, - RejectMessage, -} from "types"; - -export const getPaymentsAction = createAsyncThunk< - ApiPayments, - undefined, - { rejectValue: RejectMessage; state: RootState } ->( - "payments/getPaymentsAction", - async (_, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const payments = await getPayments(token); - refreshSessionToken(dispatch); - - return payments; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching payments: ${errorString}`, - }); - } - }, -); - -export const getPaymentsWithParamsAction = createAsyncThunk< - { - payments: ApiPayments; - searchParams: PaymentsSearchParams; - }, - PaymentsSearchParams, - { rejectValue: RejectMessage; state: RootState } ->( - "payments/getPaymentsWithParamsAction", - async (params, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - const { searchParams } = getState().payments; - - const newParams = { ...searchParams, ...params }; - - try { - const payments = await getPayments(token, newParams); - refreshSessionToken(dispatch); - - return { - payments: payments, - searchParams: newParams, - }; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching paginated payments: ${errorString}`, - }); - } - }, -); - -const initialState: PaymentsInitialState = { - items: [], - status: undefined, - pagination: undefined, - errorString: undefined, - searchParams: undefined, -}; - -const paymentsSlice = createSlice({ - name: "payments", - initialState, - reducers: {}, - extraReducers: (builder) => { - // Get payments - builder.addCase(getPaymentsAction.pending, (state = initialState) => { - state.status = "PENDING"; - }); - builder.addCase(getPaymentsAction.fulfilled, (state, action) => { - state.items = action.payload.data; - state.pagination = action.payload.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = undefined; - }); - builder.addCase(getPaymentsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - // Payments with search params - builder.addCase( - getPaymentsWithParamsAction.pending, - (state = initialState) => { - state.status = "PENDING"; - }, - ); - builder.addCase(getPaymentsWithParamsAction.fulfilled, (state, action) => { - state.items = action.payload.payments.data; - state.pagination = action.payload.payments.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = action.payload.searchParams; - }); - builder.addCase(getPaymentsWithParamsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - }, -}); - -export const paymentsSelector = (state: RootState) => state.payments; -export const { reducer } = paymentsSlice; diff --git a/src/store/index.ts b/src/store/index.ts index 049e2e9..1016f78 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -16,7 +16,6 @@ import { reducer as disbursementDrafts } from "store/ducks/disbursementDrafts"; import { reducer as disbursements } from "store/ducks/disbursements"; import { reducer as forgotPassword } from "store/ducks/forgotPassword"; import { reducer as organization } from "store/ducks/organization"; -import { reducer as payments } from "store/ducks/payments"; import { reducer as profile } from "store/ducks/profile"; import { reducer as receiverDetails } from "store/ducks/receiverDetails"; import { reducer as receiverPayments } from "store/ducks/receiverPayments"; @@ -48,7 +47,6 @@ const reducers = combineReducers({ disbursements, forgotPassword, organization, - payments, profile, receiverDetails, receiverPayments, diff --git a/src/types/index.ts b/src/types/index.ts index 57aac75..5594460 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -114,14 +114,6 @@ export type ForgotPasswordInitialState = { errorExtras?: AnyObject; }; -export type PaymentsInitialState = { - items: ApiPayment[]; - status: ActionStatus | undefined; - pagination?: Pagination; - errorString?: string; - searchParams?: PaymentsSearchParams; -}; - export type StatisticsInitialState = { stats: HomeStatistics | undefined; status: ActionStatus | undefined; @@ -220,7 +212,6 @@ export interface Store { disbursements: DisbursementsInitialState; forgotPassword: ForgotPasswordInitialState; organization: OrganizationInitialState; - payments: PaymentsInitialState; profile: ProfileInitialState; receiverDetails: ReceiverDetailsInitialState; receiverPayments: ReceiverPaymentsInitialState; From 320c254ddd8f54f9ac73a9f1dd9d41d16349ae2f Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 12 Oct 2023 09:01:05 -0400 Subject: [PATCH 2/5] SDP-785: State refactor: receivers (#23) * State refactor: payments * Cleanup * State refactor: receivers --- src/api/getReceivers.ts | 26 ------- src/apiQueries/useReceivers.ts | 33 ++++++++ src/components/ReceiversTable.tsx | 26 +++---- src/pages/Receivers.tsx | 118 ++++++++++------------------ src/store/ducks/receivers.ts | 125 ------------------------------ src/store/index.ts | 2 - src/types/index.ts | 9 --- 7 files changed, 83 insertions(+), 256 deletions(-) delete mode 100644 src/api/getReceivers.ts create mode 100644 src/apiQueries/useReceivers.ts delete mode 100644 src/store/ducks/receivers.ts diff --git a/src/api/getReceivers.ts b/src/api/getReceivers.ts deleted file mode 100644 index cf3a4f0..0000000 --- a/src/api/getReceivers.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { handleSearchParams } from "api/handleSearchParams"; -import { API_URL } from "constants/settings"; -import { ApiReceivers, ReceiversSearchParams } from "types"; - -export const getReceivers = async ( - token: string, - searchParams?: ReceiversSearchParams, -): Promise => { - // ALL status is for UI only - if (searchParams?.status === "ALL") { - delete searchParams.status; - } - - const params = handleSearchParams(searchParams); - - const response = await fetch(`${API_URL}/receivers${params}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - return handleApiResponse(response); -}; diff --git a/src/apiQueries/useReceivers.ts b/src/apiQueries/useReceivers.ts new file mode 100644 index 0000000..09b52a9 --- /dev/null +++ b/src/apiQueries/useReceivers.ts @@ -0,0 +1,33 @@ +import { useQuery } from "@tanstack/react-query"; +import { handleSearchParams } from "api/handleSearchParams"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { formatReceivers } from "helpers/formatReceivers"; +import { ApiReceivers, AppError, ReceiversSearchParams } from "types"; + +export const useReceivers = (searchParams?: ReceiversSearchParams) => { + // ALL status is for UI only + if (searchParams?.status === "ALL") { + delete searchParams.status; + } + + const params = handleSearchParams(searchParams); + + const query = useQuery({ + queryKey: ["receivers", { ...searchParams }], + queryFn: async () => { + return await fetchApi(`${API_URL}/receivers/${params}`); + }, + keepPreviousData: true, + }); + + return { + ...query, + data: query.data + ? { + ...query.data, + data: formatReceivers(query.data.data), + } + : undefined, + }; +}; diff --git a/src/components/ReceiversTable.tsx b/src/components/ReceiversTable.tsx index 1c952bd..373c4c2 100644 --- a/src/components/ReceiversTable.tsx +++ b/src/components/ReceiversTable.tsx @@ -3,13 +3,7 @@ import { formatDateTime } from "helpers/formatIntlDateTime"; import { useSort } from "hooks/useSort"; import { MultipleAmounts } from "components/MultipleAmounts"; import { Table } from "components/Table"; -import { - ActionStatus, - Receiver, - ReceiversSearchParams, - SortByReceivers, - SortDirection, -} from "types"; +import { Receiver, SortByReceivers, SortDirection } from "types"; interface ReceiversTableProps { receiversItems: Receiver[]; @@ -17,20 +11,20 @@ interface ReceiversTableProps { event: React.MouseEvent, id: string, ) => void; - searchParams: ReceiversSearchParams | undefined; + searchQuery: string | undefined; apiError: string | boolean | undefined; isFiltersSelected: boolean | undefined; - status: ActionStatus | undefined; + isLoading?: boolean; onSort?: (sort?: SortByReceivers, direction?: SortDirection) => void; } export const ReceiversTable: React.FC = ({ receiversItems, onReceiverClicked, - searchParams, + searchQuery, apiError, isFiltersSelected, - status, + isLoading, onSort, }: ReceiversTableProps) => { const { sortBy, sortDir, handleSort } = useSort(onSort); @@ -44,22 +38,22 @@ export const ReceiversTable: React.FC = ({ } if (receiversItems?.length === 0) { - if (status === "PENDING") { + if (isLoading) { return
Loading…
; } - if (searchParams?.q) { + if (searchQuery) { if (isFiltersSelected) { return (
- {`There are no receivers matching "${searchParams.q}" with selected filters`} + {`There are no receivers matching "${searchQuery}" with selected filters`}
); } return (
- {`There are no receivers matching "${searchParams.q}"`} + {`There are no receivers matching "${searchQuery}"`}
); } @@ -78,7 +72,7 @@ export const ReceiversTable: React.FC = ({ return (
- +
{/* TODO: put back once ready */} {/* diff --git a/src/pages/Receivers.tsx b/src/pages/Receivers.tsx index d343299..0c4f158 100644 --- a/src/pages/Receivers.tsx +++ b/src/pages/Receivers.tsx @@ -1,5 +1,4 @@ -import { useState, useEffect } from "react"; -import { useDispatch } from "react-redux"; +import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Button, Heading, Icon, Input, Select } from "@stellar/design-system"; @@ -9,19 +8,17 @@ import { SectionHeader } from "components/SectionHeader"; import { Pagination } from "components/Pagination"; import { ReceiversTable } from "components/ReceiversTable"; +import { useReceivers } from "apiQueries/useReceivers"; import { PAGE_LIMIT_OPTIONS, Routes } from "constants/settings"; import { number } from "helpers/formatIntlNumber"; -import { useRedux } from "hooks/useRedux"; -import { AppDispatch } from "store"; import { - getReceiversAction, - getReceiversWithParamsAction, -} from "store/ducks/receivers"; -import { CommonFilters, SortByReceivers, SortDirection } from "types"; + CommonFilters, + SortByReceivers, + SortDirection, + SortParams, +} from "types"; export const Receivers = () => { - const { receivers } = useRedux("receivers"); - const [currentPage, setCurrentPage] = useState(1); const [pageLimit, setPageLimit] = useState(20); @@ -32,33 +29,36 @@ export const Receivers = () => { }; const [filters, setFilters] = useState(initFilters); + // Using extra param to trigger API call when we want, not on every filter + // state change + const [queryFilters, setQueryFilters] = useState( + {}, + ); + const [searchQuery, setSearchQuery] = useState<{ q: string } | undefined>(); + + const { + data: receivers, + error, + isLoading, + isFetching, + } = useReceivers({ + page: currentPage.toString(), + page_limit: pageLimit.toString(), + ...queryFilters, + ...searchQuery, + }); const isFiltersSelected = Object.values(filters).filter((v) => Boolean(v)).length > 0; - const dispatch: AppDispatch = useDispatch(); const navigate = useNavigate(); - useEffect(() => { - dispatch(getReceiversAction()); - }, [dispatch]); - - const apiError = receivers.status === "ERROR" && receivers.errorString; - const maxPages = receivers.pagination?.pages || 1; - const isSearchInProgress = Boolean( - receivers.searchParams?.q && receivers.status === "PENDING", - ); + const maxPages = receivers?.pagination?.pages || 1; + const isSearchInProgress = Boolean(searchQuery && (isLoading || isFetching)); const handleSearchChange = (searchText?: string) => { - dispatch( - getReceiversWithParamsAction({ - page: "1", - ...filters, - q: searchText, - }), - ); - setCurrentPage(1); + setSearchQuery(searchText ? { q: searchText } : undefined); }; const handleFilterChange = ( @@ -71,48 +71,21 @@ export const Receivers = () => { }; const handleFilterSubmit = () => { - dispatch( - getReceiversWithParamsAction({ - page: "1", - ...filters, - }), - ); - setCurrentPage(1); + setQueryFilters(filters); }; const handleFilterReset = () => { - dispatch( - getReceiversWithParamsAction({ - page: "1", - ...initFilters, - }), - ); - - setFilters(initFilters); setCurrentPage(1); + setFilters(initFilters); + setQueryFilters(initFilters); }; const handleSort = (sort?: SortByReceivers, direction?: SortDirection) => { - if (!sort || !direction || direction === "default") { - dispatch( - getReceiversWithParamsAction({ - page: "1", - sort: undefined, - direction: undefined, - }), - ); - } else { - dispatch( - getReceiversWithParamsAction({ - page: "1", - sort, - direction, - }), - ); - } + const isDefaultSort = !sort || !direction || direction === "default"; setCurrentPage(1); + setQueryFilters(isDefaultSort ? filters : { ...filters, sort, direction }); }; const handleExport = ( @@ -128,16 +101,8 @@ export const Receivers = () => { event.preventDefault(); const pageLimit = Number(event.target.value); - setPageLimit(pageLimit); setCurrentPage(1); - - // Need to make sure we'll be loading page 1 - dispatch( - getReceiversWithParamsAction({ - page_limit: pageLimit.toString(), - page: "1", - }), - ); + setPageLimit(pageLimit); }; const handleReceiverClicked = ( @@ -154,7 +119,7 @@ export const Receivers = () => { - {receivers.pagination?.total && receivers.pagination.total > 0 + {receivers?.pagination?.total && receivers.pagination.total > 0 ? `${number.format(receivers.pagination.total)} ` : ""} Receivers @@ -251,23 +216,20 @@ export const Receivers = () => { maxPages={Number(maxPages)} onSetPage={(page) => { setCurrentPage(page); - dispatch( - getReceiversWithParamsAction({ page: page.toString() }), - ); }} - isLoading={receivers.status === "PENDING"} + isLoading={isLoading || isFetching} /> diff --git a/src/store/ducks/receivers.ts b/src/store/ducks/receivers.ts deleted file mode 100644 index bfa64bf..0000000 --- a/src/store/ducks/receivers.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { RootState } from "store"; -import { getReceivers } from "api/getReceivers"; -import { handleApiErrorString } from "api/handleApiErrorString"; -import { formatReceivers } from "helpers/formatReceivers"; -import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; -import { refreshSessionToken } from "helpers/refreshSessionToken"; -import { - ApiError, - ApiReceivers, - ReceiversInitialState, - ReceiversSearchParams, - RejectMessage, -} from "types"; - -export const getReceiversAction = createAsyncThunk< - ApiReceivers, - undefined, - { rejectValue: RejectMessage; state: RootState } ->( - "receivers/getReceiversAction", - async (_, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const receivers = await getReceivers(token); - refreshSessionToken(dispatch); - - return receivers; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching receivers: ${errorString}`, - }); - } - }, -); - -export const getReceiversWithParamsAction = createAsyncThunk< - { - receivers: ApiReceivers; - searchParams: ReceiversSearchParams; - }, - ReceiversSearchParams, - { rejectValue: RejectMessage; state: RootState } ->( - "receivers/getReceiversWithParamsAction", - async (params, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - const { searchParams } = getState().receivers; - - const newParams = { ...searchParams, ...params }; - - try { - const receivers = await getReceivers(token, newParams); - refreshSessionToken(dispatch); - - return { - receivers: receivers, - searchParams: newParams, - }; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching paginated receivers: ${errorString}`, - }); - } - }, -); - -const initialState: ReceiversInitialState = { - items: [], - status: undefined, - pagination: undefined, - errorString: undefined, - searchParams: undefined, -}; - -const receiversSlice = createSlice({ - name: "receivers", - initialState, - reducers: {}, - extraReducers: (builder) => { - // Get receivers - builder.addCase(getReceiversAction.pending, (state = initialState) => { - state.status = "PENDING"; - }); - builder.addCase(getReceiversAction.fulfilled, (state, action) => { - state.items = formatReceivers(action.payload.data); - state.pagination = action.payload.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = undefined; - }); - builder.addCase(getReceiversAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - // Receivers with search params - builder.addCase( - getReceiversWithParamsAction.pending, - (state = initialState) => { - state.status = "PENDING"; - }, - ); - builder.addCase(getReceiversWithParamsAction.fulfilled, (state, action) => { - state.items = formatReceivers(action.payload.receivers.data); - state.pagination = action.payload.receivers.pagination; - state.status = "SUCCESS"; - state.errorString = undefined; - state.searchParams = action.payload.searchParams; - }); - builder.addCase(getReceiversWithParamsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - }, -}); - -export const receiversSelector = (state: RootState) => state.receivers; -export const { reducer } = receiversSlice; diff --git a/src/store/index.ts b/src/store/index.ts index 1016f78..bfe9cb2 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,7 +19,6 @@ import { reducer as organization } from "store/ducks/organization"; import { reducer as profile } from "store/ducks/profile"; import { reducer as receiverDetails } from "store/ducks/receiverDetails"; import { reducer as receiverPayments } from "store/ducks/receiverPayments"; -import { reducer as receivers } from "store/ducks/receivers"; import { reducer as statistics } from "store/ducks/statistics"; import { reducer as userAccount } from "store/ducks/userAccount"; import { reducer as users } from "store/ducks/users"; @@ -50,7 +49,6 @@ const reducers = combineReducers({ profile, receiverDetails, receiverPayments, - receivers, statistics, userAccount, users, diff --git a/src/types/index.ts b/src/types/index.ts index 5594460..4e92070 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -120,14 +120,6 @@ export type StatisticsInitialState = { errorString?: string; }; -export type ReceiversInitialState = { - items: Receiver[]; - status: ActionStatus | undefined; - pagination?: Pagination; - errorString?: string; - searchParams?: ReceiversSearchParams; -}; - export type ReceiverDetailsInitialState = { id: string; phoneNumber: string; @@ -215,7 +207,6 @@ export interface Store { profile: ProfileInitialState; receiverDetails: ReceiverDetailsInitialState; receiverPayments: ReceiverPaymentsInitialState; - receivers: ReceiversInitialState; statistics: StatisticsInitialState; userAccount: UserAccountInitialState; users: UsersInitialState; From 4a463dc2a30097604625a70b9dba24da4b18739b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cec=C3=ADlia=20Rom=C3=A3o?= Date: Thu, 12 Oct 2023 11:19:55 -0300 Subject: [PATCH 3/5] SDP-830: New Disbursement - Filtering of assets based on wallet selection (#24) --- src/api/getAssetsByWallet.ts | 18 +++++ src/components/DisbursementDetails/index.tsx | 75 ++++++++++---------- src/store/ducks/assets.ts | 38 ++++++++++ 3 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 src/api/getAssetsByWallet.ts diff --git a/src/api/getAssetsByWallet.ts b/src/api/getAssetsByWallet.ts new file mode 100644 index 0000000..eff9538 --- /dev/null +++ b/src/api/getAssetsByWallet.ts @@ -0,0 +1,18 @@ +import { handleApiResponse } from "api/handleApiResponse"; +import { API_URL } from "constants/settings"; +import { ApiAsset } from "types"; + +export const getAssetsByWallet = async ( + token: string, + walletId: string, +): Promise => { + const response = await fetch(`${API_URL}/assets?wallet=${walletId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + return handleApiResponse(response); +}; diff --git a/src/components/DisbursementDetails/index.tsx b/src/components/DisbursementDetails/index.tsx index 01874c0..1ac94cb 100644 --- a/src/components/DisbursementDetails/index.tsx +++ b/src/components/DisbursementDetails/index.tsx @@ -10,7 +10,7 @@ import { useDispatch } from "react-redux"; import { AppDispatch } from "store"; import { getCountriesAction } from "store/ducks/countries"; -import { getAssetsAction } from "store/ducks/assets"; +import { getAssetsByWalletAction } from "store/ducks/assets"; import { getWalletsAction } from "store/ducks/wallets"; import { InfoTooltip } from "components/InfoTooltip"; @@ -82,19 +82,15 @@ export const DisbursementDetails: React.FC = ({ dispatch(getCountriesAction()); } - if (!assets.status) { - dispatch(getAssetsAction()); - } - if (!wallets.status) { dispatch(getWalletsAction()); } - }, [assets.status, countries.status, wallets.status, dispatch]); + }, [dispatch, countries.status, wallets.status]); const apiErrors = [ - assets.errorString, countries.errorString, wallets.errorString, + assets.errorString, ]; const sanitizedApiErrors = apiErrors.filter((e) => Boolean(e)); @@ -106,10 +102,10 @@ export const DisbursementDetails: React.FC = ({ missingFields.push(FieldId.NAME); } else if (!inputs.country.code) { missingFields.push(FieldId.COUNTRY_CODE); - } else if (!inputs.asset.code) { - missingFields.push(FieldId.ASSET_CODE); } else if (!inputs.wallet.id) { missingFields.push(FieldId.WALLET_ID); + } else if (!inputs.asset.code) { + missingFields.push(FieldId.ASSET_CODE); } const isValid = missingFields.length === 0; @@ -153,26 +149,27 @@ export const DisbursementDetails: React.FC = ({ }); break; - case FieldId.ASSET_CODE: + case FieldId.WALLET_ID: // eslint-disable-next-line no-case-declarations - const asset = assets.items.find((a: ApiAsset) => a.id === value); + const wallet = wallets.items.find((w: ApiWallet) => w.id === value); updateState({ - asset: { - id: asset?.id || "", - code: asset?.code || "", + wallet: { + id: wallet?.id || "", + name: wallet?.name || "", }, }); + dispatch(getAssetsByWalletAction({ walletId: wallet?.id || "" })); break; - case FieldId.WALLET_ID: + case FieldId.ASSET_CODE: // eslint-disable-next-line no-case-declarations - const wallet = wallets.items.find((w: ApiWallet) => w.id === value); + const asset = assets.items.find((a: ApiAsset) => a.id === value); updateState({ - wallet: { - id: wallet?.id || "", - name: wallet?.name || "", + asset: { + id: asset?.id || "", + code: asset?.code || "", }, }); @@ -203,16 +200,16 @@ export const DisbursementDetails: React.FC = ({
- +
- {details.asset.code} + {details.wallet.name}
- +
- {details.wallet.name} + {details.asset.code}
@@ -254,22 +251,6 @@ export const DisbursementDetails: React.FC = ({ ))} - - + + ( + "assets/getAssetsByWalletAction", + async ({ walletId }, { rejectWithValue, getState, dispatch }) => { + const { token } = getState().userAccount; + + try { + const assets = await getAssetsByWallet(token, walletId); + // Don't show soft-deleted assets + return assets.filter((a) => !a.deleted_at); + } catch (error: unknown) { + const errorString = handleApiErrorString(error as ApiError); + endSessionIfTokenInvalid(errorString, dispatch); + + return rejectWithValue({ + errorString: `Error fetching assets: ${errorString}`, + }); + } + }, +); + const initialState: AssetsInitialState = { items: [], status: undefined, @@ -52,6 +77,19 @@ const assetsSlice = createSlice({ state.status = "ERROR"; state.errorString = action.payload?.errorString; }); + + builder.addCase(getAssetsByWalletAction.pending, (state = initialState) => { + state.status = "PENDING"; + }); + builder.addCase(getAssetsByWalletAction.fulfilled, (state, action) => { + state.items = action.payload; + state.status = "SUCCESS"; + state.errorString = undefined; + }); + builder.addCase(getAssetsByWalletAction.rejected, (state, action) => { + state.status = "ERROR"; + state.errorString = action.payload?.errorString; + }); }, }); From 9e0613ee6cbfac64f8d71eae351c4ff4f5b94b89 Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 12 Oct 2023 14:06:25 -0400 Subject: [PATCH 4/5] SDP-786: State refactor: statistics (#25) * State refactor: statistics * Keep 5 min token check --- src/api/getStatistics.ts | 15 ----- src/apiQueries/useStatistics.ts | 20 +++++++ src/components/DashboardAnalytics.tsx | 46 +++++++-------- src/helpers/fetchApi.ts | 1 + src/helpers/formatStatistics.ts | 21 +++++++ src/store/ducks/statistics.ts | 85 --------------------------- src/store/index.ts | 2 - src/types/index.ts | 7 --- 8 files changed, 64 insertions(+), 133 deletions(-) delete mode 100644 src/api/getStatistics.ts create mode 100644 src/apiQueries/useStatistics.ts create mode 100644 src/helpers/formatStatistics.ts delete mode 100644 src/store/ducks/statistics.ts diff --git a/src/api/getStatistics.ts b/src/api/getStatistics.ts deleted file mode 100644 index 55f2e85..0000000 --- a/src/api/getStatistics.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { ApiStatistics } from "types"; - -export const getStatistics = async (token: string): Promise => { - const response = await fetch(`${API_URL}/statistics`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - return handleApiResponse(response); -}; diff --git a/src/apiQueries/useStatistics.ts b/src/apiQueries/useStatistics.ts new file mode 100644 index 0000000..f35badb --- /dev/null +++ b/src/apiQueries/useStatistics.ts @@ -0,0 +1,20 @@ +import { useQuery } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { formatStatistics } from "helpers/formatStatistics"; +import { ApiStatistics, AppError } from "types"; + +export const useStatistics = (isAuthenticated: boolean) => { + const query = useQuery({ + queryKey: ["statistics"], + queryFn: async () => { + return await fetchApi(`${API_URL}/statistics`); + }, + enabled: Boolean(isAuthenticated), + }); + + return { + ...query, + data: query.data ? formatStatistics(query.data) : undefined, + }; +}; diff --git a/src/components/DashboardAnalytics.tsx b/src/components/DashboardAnalytics.tsx index c22c5fc..e7e25a7 100644 --- a/src/components/DashboardAnalytics.tsx +++ b/src/components/DashboardAnalytics.tsx @@ -1,24 +1,21 @@ -import { useEffect } from "react"; -import { useDispatch } from "react-redux"; import { Card, Notification } from "@stellar/design-system"; import { InfoTooltip } from "components/InfoTooltip"; import { AssetAmount } from "components/AssetAmount"; +import { useStatistics } from "apiQueries/useStatistics"; import { percent } from "helpers/formatIntlNumber"; import { renderNumberOrDash } from "helpers/renderNumberOrDash"; import { useRedux } from "hooks/useRedux"; -import { - clearStatisticsAction, - getStatisticsAction, -} from "store/ducks/statistics"; -import { AppDispatch } from "store"; export const DashboardAnalytics = () => { - const { statistics, userAccount } = useRedux("statistics", "userAccount"); - const { stats } = statistics; - const dispatch: AppDispatch = useDispatch(); + const { userAccount } = useRedux("userAccount"); - const apiErrorStats = statistics.status === "ERROR" && statistics.errorString; + const { + data: stats, + error, + isLoading, + isFetching, + } = useStatistics(userAccount.isAuthenticated); const calculateRate = () => { if (stats?.paymentsSuccessfulCounts && stats?.paymentsTotalCount) { @@ -28,24 +25,22 @@ export const DashboardAnalytics = () => { return 0; }; - useEffect(() => { - if (userAccount.isAuthenticated) { - dispatch(getStatisticsAction()); - } - - return () => { - dispatch(clearStatisticsAction()); - }; - }, [dispatch, userAccount.isAuthenticated]); - - if (apiErrorStats) { + if (error) { return ( - {apiErrorStats} + {error.message} ); } + if (isLoading || isFetching) { + return ( +
+
Loading…
+
+ ); + } + return (
{/* TODO: add disbursement volume chart */} @@ -122,7 +117,10 @@ export const DashboardAnalytics = () => { {stats?.assets.map((a) => (
- +
diff --git a/src/helpers/fetchApi.ts b/src/helpers/fetchApi.ts index 229450c..ddb5e46 100644 --- a/src/helpers/fetchApi.ts +++ b/src/helpers/fetchApi.ts @@ -35,6 +35,7 @@ export const fetchApi = async ( sessionExpired(); } else if (minRemaining < 5) { token = await refreshToken(token); + localStorageSessionToken.set(token); } config.headers = { diff --git a/src/helpers/formatStatistics.ts b/src/helpers/formatStatistics.ts new file mode 100644 index 0000000..d862eb2 --- /dev/null +++ b/src/helpers/formatStatistics.ts @@ -0,0 +1,21 @@ +import { ApiStatistics, HomeStatistics } from "types"; + +export const formatStatistics = (statistics: ApiStatistics): HomeStatistics => { + return { + paymentsSuccessfulCounts: statistics.payment_counters.success, + paymentsFailedCount: statistics.payment_counters.failed, + paymentsRemainingCount: Number( + statistics.payment_counters.total - + statistics.payment_counters.success - + statistics.payment_counters.failed, + ), + paymentsTotalCount: statistics.payment_counters.total, + walletsTotalCount: statistics.receiver_wallets_counters.total, + individualsTotalCount: statistics.total_receivers, + assets: statistics.payment_amounts_by_asset.map((a) => ({ + assetCode: a.asset_code, + success: a.payment_amounts.success.toString(), + average: a.payment_amounts.average.toString(), + })), + }; +}; diff --git a/src/store/ducks/statistics.ts b/src/store/ducks/statistics.ts deleted file mode 100644 index aa32fc3..0000000 --- a/src/store/ducks/statistics.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { getStatistics } from "api/getStatistics"; -import { handleApiErrorString } from "api/handleApiErrorString"; -import { RootState } from "store"; -import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; -import { refreshSessionToken } from "helpers/refreshSessionToken"; -import { - ApiError, - HomeStatistics, - RejectMessage, - StatisticsInitialState, -} from "types"; - -export const getStatisticsAction = createAsyncThunk< - HomeStatistics, - undefined, - { rejectValue: RejectMessage; state: RootState } ->( - "statistics/getStatisticsAction", - async (_, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const statistics = await getStatistics(token); - refreshSessionToken(dispatch); - - return { - paymentsSuccessfulCounts: statistics.payment_counters.success, - paymentsFailedCount: statistics.payment_counters.failed, - paymentsRemainingCount: Number( - statistics.payment_counters.total - - statistics.payment_counters.success - - statistics.payment_counters.failed, - ), - paymentsTotalCount: statistics.payment_counters.total, - walletsTotalCount: statistics.receiver_wallets_counters.total, - individualsTotalCount: statistics.total_receivers, - assets: statistics.payment_amounts_by_asset.map((a) => ({ - assetCode: a.asset_code, - success: a.payment_amounts.success.toString(), - average: a.payment_amounts.average.toString(), - })), - }; - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching statistics: ${errorString}`, - }); - } - }, -); - -const initialState: StatisticsInitialState = { - stats: undefined, - status: undefined, - errorString: undefined, -}; - -const statisticsSlice = createSlice({ - name: "statistics", - initialState, - reducers: { - clearStatisticsAction: () => initialState, - }, - extraReducers: (builder) => { - builder.addCase(getStatisticsAction.pending, (state = initialState) => { - state.status = "PENDING"; - }); - builder.addCase(getStatisticsAction.fulfilled, (state, action) => { - state.stats = action.payload; - state.status = "SUCCESS"; - state.errorString = undefined; - }); - builder.addCase(getStatisticsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - }, -}); - -export const statisticsSelector = (state: RootState) => state.statistics; -export const { reducer } = statisticsSlice; -export const { clearStatisticsAction } = statisticsSlice.actions; diff --git a/src/store/index.ts b/src/store/index.ts index bfe9cb2..3012560 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -19,7 +19,6 @@ import { reducer as organization } from "store/ducks/organization"; import { reducer as profile } from "store/ducks/profile"; import { reducer as receiverDetails } from "store/ducks/receiverDetails"; import { reducer as receiverPayments } from "store/ducks/receiverPayments"; -import { reducer as statistics } from "store/ducks/statistics"; import { reducer as userAccount } from "store/ducks/userAccount"; import { reducer as users } from "store/ducks/users"; import { reducer as wallets } from "store/ducks/wallets"; @@ -49,7 +48,6 @@ const reducers = combineReducers({ profile, receiverDetails, receiverPayments, - statistics, userAccount, users, wallets, diff --git a/src/types/index.ts b/src/types/index.ts index 4e92070..fc5f042 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -114,12 +114,6 @@ export type ForgotPasswordInitialState = { errorExtras?: AnyObject; }; -export type StatisticsInitialState = { - stats: HomeStatistics | undefined; - status: ActionStatus | undefined; - errorString?: string; -}; - export type ReceiverDetailsInitialState = { id: string; phoneNumber: string; @@ -207,7 +201,6 @@ export interface Store { profile: ProfileInitialState; receiverDetails: ReceiverDetailsInitialState; receiverPayments: ReceiverPaymentsInitialState; - statistics: StatisticsInitialState; userAccount: UserAccountInitialState; users: UsersInitialState; wallets: WalletsInitialState; From 60a0a316f6c6269c633eb7e575df3b4d084aef87 Mon Sep 17 00:00:00 2001 From: Iveta Date: Fri, 13 Oct 2023 13:42:36 -0400 Subject: [PATCH 5/5] State refactor: edit receiver details (#26) --- src/api/patchReceiver.ts | 31 --- src/apiQueries/useReceiversReceiverId.ts | 25 ++- src/apiQueries/useUpdateReceiverDetails.ts | 41 ++++ src/components/LoadingContent.tsx | 3 + src/helpers/formatReceiver.ts | 35 ++++ src/pages/PaymentDetails.tsx | 43 ++-- src/pages/ReceiverDetailsEdit.tsx | 233 ++++++++++++--------- src/store/ducks/receiverDetails.ts | 83 +------- src/styles.scss | 4 + 9 files changed, 258 insertions(+), 240 deletions(-) delete mode 100644 src/api/patchReceiver.ts create mode 100644 src/apiQueries/useUpdateReceiverDetails.ts create mode 100644 src/components/LoadingContent.tsx create mode 100644 src/helpers/formatReceiver.ts diff --git a/src/api/patchReceiver.ts b/src/api/patchReceiver.ts deleted file mode 100644 index 29bccc7..0000000 --- a/src/api/patchReceiver.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { sanitizeObject } from "helpers/sanitizeObject"; - -export const patchReceiverInfo = async ( - token: string, - receiverId: string, - fields: { - email: string; - externalId: string; - }, -): Promise<{ message: string }> => { - const fieldsToSubmit = sanitizeObject({ - email: fields.email, - external_id: fields.externalId, - }); - - if (Object.keys(fieldsToSubmit).length === 0) { - throw Error("Update receiver info requires at least one field to submit"); - } - - const response = await fetch(`${API_URL}/receivers/${receiverId}`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(fieldsToSubmit), - }); - - return handleApiResponse(response); -}; diff --git a/src/apiQueries/useReceiversReceiverId.ts b/src/apiQueries/useReceiversReceiverId.ts index 059f0d0..98d40f4 100644 --- a/src/apiQueries/useReceiversReceiverId.ts +++ b/src/apiQueries/useReceiversReceiverId.ts @@ -1,16 +1,35 @@ import { useQuery } from "@tanstack/react-query"; import { API_URL } from "constants/settings"; import { fetchApi } from "helpers/fetchApi"; +import { formatPaymentReceiver } from "helpers/formatPaymentReceiver"; +import { formatReceiver } from "helpers/formatReceiver"; import { ApiReceiver, AppError } from "types"; -export const useReceiversReceiverId = (receiverId: string | undefined) => { +export const useReceiversReceiverId = ({ + receiverId, + dataFormat, + receiverWalletId, +}: { + receiverId: string | undefined; + dataFormat: "receiver" | "paymentReceiver"; + receiverWalletId?: string; +}) => { const query = useQuery({ - queryKey: ["receivers", receiverId], + queryKey: ["receivers", dataFormat, receiverId, { receiverWalletId }], queryFn: async () => { return await fetchApi(`${API_URL}/receivers/${receiverId}`); }, enabled: !!receiverId, }); - return query; + const formatData = (data: ApiReceiver) => { + return dataFormat === "receiver" + ? formatReceiver(data) + : formatPaymentReceiver(data, receiverWalletId); + }; + + return { + ...query, + data: query.data ? (formatData(query.data) as T) : undefined, + }; }; diff --git a/src/apiQueries/useUpdateReceiverDetails.ts b/src/apiQueries/useUpdateReceiverDetails.ts new file mode 100644 index 0000000..8be091b --- /dev/null +++ b/src/apiQueries/useUpdateReceiverDetails.ts @@ -0,0 +1,41 @@ +import { useMutation } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { sanitizeObject } from "helpers/sanitizeObject"; +import { AppError } from "types"; + +export const useUpdateReceiverDetails = (receiverId: string | undefined) => { + const mutation = useMutation({ + mutationFn: (fields: { email: string; externalId: string }) => { + const fieldsToSubmit = sanitizeObject({ + email: fields.email, + external_id: fields.externalId, + }); + + if (Object.keys(fieldsToSubmit).length === 0) { + return new Promise((_, reject) => + reject({ + message: + "Update receiver info requires at least one field to submit", + }), + ); + } + + return fetchApi( + `${API_URL}/receivers/${receiverId}`, + { + method: "PATCH", + body: JSON.stringify(fieldsToSubmit), + }, + { omitContentType: true }, + ); + }, + cacheTime: 0, + }); + + return { + ...mutation, + error: mutation.error as AppError, + data: mutation.data as { message: string }, + }; +}; diff --git a/src/components/LoadingContent.tsx b/src/components/LoadingContent.tsx new file mode 100644 index 0000000..34ec015 --- /dev/null +++ b/src/components/LoadingContent.tsx @@ -0,0 +1,3 @@ +export const LoadingContent = () => { + return
Loading…
; +}; diff --git a/src/helpers/formatReceiver.ts b/src/helpers/formatReceiver.ts new file mode 100644 index 0000000..222f2b3 --- /dev/null +++ b/src/helpers/formatReceiver.ts @@ -0,0 +1,35 @@ +import { ApiReceiver, ReceiverDetails } from "types"; + +export const formatReceiver = (receiver: ApiReceiver): ReceiverDetails => ({ + id: receiver.id, + phoneNumber: receiver.phone_number, + email: receiver.email, + orgId: receiver.external_id, + // TODO: how to handle multiple + assetCode: receiver.received_amounts?.[0].asset_code, + totalReceived: receiver.received_amounts?.[0].received_amount, + stats: { + paymentsTotalCount: Number(receiver.total_payments), + paymentsSuccessfulCount: Number(receiver.successful_payments), + paymentsFailedCount: Number(receiver.failed_payments), + paymentsRemainingCount: Number(receiver.remaining_payments), + }, + wallets: receiver.wallets.map((w) => ({ + id: w.id, + stellarAddress: w.stellar_address, + provider: w.wallet.name, + invitedAt: w.invited_at, + createdAt: w.created_at, + smsLastSentAt: w.last_sms_sent, + totalPaymentsCount: Number(w.payments_received), + // TODO: how to handle multiple + assetCode: w.received_amounts[0].asset_code, + totalAmountReceived: w.received_amounts[0].received_amount, + // TODO: withdrawn amount + withdrawnAmount: "", + })), + verifications: receiver.verifications.map((v) => ({ + verificationField: v.VerificationField, + value: v.HashedValue, + })), +}); diff --git a/src/pages/PaymentDetails.tsx b/src/pages/PaymentDetails.tsx index 75437f5..35a345e 100644 --- a/src/pages/PaymentDetails.tsx +++ b/src/pages/PaymentDetails.tsx @@ -12,7 +12,6 @@ import { useReceiversReceiverId } from "apiQueries/useReceiversReceiverId"; import { Routes, STELLAR_EXPERT_URL } from "constants/settings"; import { formatDateTime } from "helpers/formatIntlDateTime"; import { shortenString } from "helpers/shortenString"; -import { formatPaymentReceiver } from "helpers/formatPaymentReceiver"; import { formatPaymentDetails } from "helpers/formatPaymentDetails"; import { Breadcrumbs } from "components/Breadcrumbs"; @@ -23,6 +22,7 @@ import { ReceiverStatus } from "components/ReceiverStatus"; import { AssetAmount } from "components/AssetAmount"; import { MultipleAmounts } from "components/MultipleAmounts"; import { RetryFailedPayment } from "components/RetryFailedPayment"; +import { PaymentDetailsReceiver } from "types"; // TODO: handle loading/fetching state (create component that handles it // everywhere) @@ -33,14 +33,13 @@ export const PaymentDetails = () => { const { data: payment, error: paymentError } = usePaymentsPaymentId(paymentId); - const { data: receiver } = useReceiversReceiverId( - payment?.receiver_wallet?.receiver?.id, - ); - const formattedPayment = payment ? formatPaymentDetails(payment) : null; - const formattedReceiver = receiver - ? formatPaymentReceiver(receiver, formattedPayment?.receiverWalletId) - : null; + + const { data: receiver } = useReceiversReceiverId({ + receiverId: payment?.receiver_wallet?.receiver?.id, + dataFormat: "paymentReceiver", + receiverWalletId: formattedPayment?.receiverWalletId, + }); const navigate = useNavigate(); @@ -250,24 +249,24 @@ export const PaymentDetails = () => { */} - {formattedReceiver?.phoneNumber ? ( + {receiver?.phoneNumber ? ( - goToReceiverDetails(event, formattedReceiver.id) + goToReceiverDetails(event, receiver.id) } > - {formattedReceiver.phoneNumber} + {receiver.phoneNumber} ) : ( "-" )} - {formattedReceiver?.walletAddress ? ( + {receiver?.walletAddress ? ( { )} - {formattedReceiver?.provider || "-"} + {receiver?.provider || "-"} - {formattedReceiver?.totalPaymentsCount || "-"} + {receiver?.totalPaymentsCount || "-"} - {formattedReceiver?.successfulPaymentsCount || "-"} + {receiver?.successfulPaymentsCount || "-"} - {formattedReceiver?.createdAt - ? formatDateTime(formattedReceiver.createdAt) + {receiver?.createdAt + ? formatDateTime(receiver.createdAt) : "-"} - {formattedReceiver?.status ? ( - + {receiver?.status ? ( + ) : ( "-" )} diff --git a/src/pages/ReceiverDetailsEdit.tsx b/src/pages/ReceiverDetailsEdit.tsx index ad68823..902ebbe 100644 --- a/src/pages/ReceiverDetailsEdit.tsx +++ b/src/pages/ReceiverDetailsEdit.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; import { Card, Heading, @@ -10,103 +9,126 @@ import { Notification, } from "@stellar/design-system"; -import { AppDispatch } from "store"; -import { - getReceiverDetailsAction, - updateReceiverDetailsAction, -} from "store/ducks/receiverDetails"; -import { Routes } from "constants/settings"; -import { useRedux } from "hooks/useRedux"; +import { useReceiversReceiverId } from "apiQueries/useReceiversReceiverId"; +import { GENERIC_ERROR_MESSAGE, Routes } from "constants/settings"; import { Breadcrumbs } from "components/Breadcrumbs"; import { SectionHeader } from "components/SectionHeader"; import { CopyWithIcon } from "components/CopyWithIcon"; import { InfoTooltip } from "components/InfoTooltip"; -import { ReceiverEditFields } from "types"; +import { LoadingContent } from "components/LoadingContent"; + +import { ReceiverDetails, ReceiverEditFields } from "types"; +import { useUpdateReceiverDetails } from "apiQueries/useUpdateReceiverDetails"; export const ReceiverDetailsEdit = () => { const { id: receiverId } = useParams(); - const { receiverDetails } = useRedux("receiverDetails"); - const dispatch: AppDispatch = useDispatch(); const navigate = useNavigate(); - const [pin, setPin] = useState(""); - const [dateOfBirth, setDateOfBirth] = useState(""); - const [nationalId, setNationalId] = useState(""); const [receiverEditFields, setReceiverEditFields] = useState({ email: "", externalId: "", }); + const { + data: receiverDetails, + isSuccess: isReceiverDetailsSuccess, + isLoading: isReceiverDetailsLoading, + error: receiverDetailsError, + refetch, + } = useReceiversReceiverId({ + receiverId, + dataFormat: "receiver", + }); + + const { + isSuccess: isUpdateSuccess, + isLoading: isUpdateLoading, + error: updateError, + mutateAsync, + reset, + } = useUpdateReceiverDetails(receiverId); + useEffect(() => { - if (receiverId) { - dispatch(getReceiverDetailsAction(receiverId)); + if (isReceiverDetailsSuccess) { + setReceiverEditFields({ + email: receiverDetails?.email || "", + externalId: receiverDetails?.orgId || "", + }); } - }, [receiverId, dispatch]); + }, [ + isReceiverDetailsSuccess, + receiverDetails?.email, + receiverDetails?.orgId, + ]); useEffect(() => { - setReceiverEditFields({ - email: receiverDetails.email || "", - externalId: receiverDetails.orgId, - }); - }, [receiverDetails.email, receiverDetails.orgId]); + if (isUpdateSuccess && receiverId) { + reset(); + refetch(); + navigate(`${Routes.RECEIVERS}/${receiverId}`); + } + }, [isUpdateSuccess, receiverId, navigate, reset, refetch]); useEffect(() => { - receiverDetails.verifications.forEach((v) => { - switch (v.verificationField) { - case "DATE_OF_BIRTH": - setDateOfBirth(v.value); - break; - case "PIN": - setPin(v.value); - break; - case "NATIONAL_ID_NUMBER": - setNationalId(v.value); - break; + return () => { + if (updateError) { + reset(); } - }); - }, [receiverDetails.verifications]); + }; + }, [updateError, reset]); + + const getReadyOnlyValue = ( + field: "DATE_OF_BIRTH" | "PIN" | "NATIONAL_ID_NUMBER", + ) => { + return ( + receiverDetails?.verifications.find((v) => v.verificationField === field) + ?.value || "" + ); + }; const emptyValueIfNotChanged = (newValue: string, oldValue: string) => { return newValue === oldValue ? "" : newValue; }; - const handleReceiverEditSubmit = ( - e: React.MouseEvent, + const handleReceiverEditSubmit = async ( + e: React.FormEvent, ) => { e.preventDefault(); const { email, externalId } = receiverEditFields; - if ((email || externalId) && receiverId) { - dispatch( - updateReceiverDetailsAction({ - receiverId, - email: emptyValueIfNotChanged(email, receiverDetails.email || ""), - externalId: emptyValueIfNotChanged(externalId, receiverDetails.orgId), - }), - ); - } - - if (!receiverDetails.errorString) { - navigate(`${Routes.RECEIVERS}/${receiverId}`); + if (receiverId) { + try { + await mutateAsync({ + email: emptyValueIfNotChanged(email, receiverDetails?.email || ""), + externalId: emptyValueIfNotChanged( + externalId, + receiverDetails?.orgId || "", + ), + }); + } catch (e) { + // do nothing + } } }; - const handleReceiverEditCancel = ( - e: React.MouseEvent, - ) => { + const handleReceiverEditCancel = (e: React.FormEvent) => { e.preventDefault(); setReceiverEditFields({ - email: receiverDetails.email || "", - externalId: receiverDetails.orgId, + email: receiverDetails?.email || "", + externalId: receiverDetails?.orgId || "", }); navigate(`${Routes.RECEIVERS}/${receiverId}`); }; const handleDetailsChange = (event: React.ChangeEvent) => { + if (updateError) { + reset(); + } + setReceiverEditFields({ ...receiverEditFields, [event.target.name]: event.target.value, @@ -114,8 +136,20 @@ export const ReceiverDetailsEdit = () => { }; const renderInfoEditContent = () => { + if (isReceiverDetailsLoading) { + return ; + } + + if (receiverDetailsError || !receiverDetails) { + return ( + +
{receiverDetailsError?.message || GENERIC_ERROR_MESSAGE}
+
+ ); + } + const isSubmitDisabled = - receiverEditFields.email === receiverDetails.email && + receiverEditFields.email === receiverDetails?.email && receiverEditFields.externalId === receiverDetails.orgId; return ( @@ -135,20 +169,25 @@ export const ReceiverDetailsEdit = () => { - {receiverDetails.errorString && ( - -
{receiverDetails.errorString}
-
- )} -
- -
-
- Receiver info -
+ {updateError ? ( + +
{updateError?.message}
+
+ ) : null} + +
+ +
+
+ {/* TODO: info text */} + Receiver info +
-
{ name="personalPIN" label="Personal PIN" fieldSize="sm" - value={pin} + value={getReadyOnlyValue("PIN")} disabled /> { name="nationalIDNumber" label="National ID Number" fieldSize="sm" - value={nationalId} + value={getReadyOnlyValue("NATIONAL_ID_NUMBER")} disabled /> { name="dateOfBirth" label="Date of Birth" fieldSize="sm" - value={dateOfBirth} + value={getReadyOnlyValue("DATE_OF_BIRTH")} disabled />
- +
+
+
+ +
- -
- - -
+
); diff --git a/src/store/ducks/receiverDetails.ts b/src/store/ducks/receiverDetails.ts index 9d692d2..c7135a7 100644 --- a/src/store/ducks/receiverDetails.ts +++ b/src/store/ducks/receiverDetails.ts @@ -1,14 +1,13 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { RootState } from "store"; import { getReceiverDetails } from "api/getReceiverDetails"; -import { patchReceiverInfo } from "api/patchReceiver"; import { retryInvitationSMS } from "api/retryInvitationSMS"; import { handleApiErrorString } from "api/handleApiErrorString"; import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; import { refreshSessionToken } from "helpers/refreshSessionToken"; +import { formatReceiver } from "helpers/formatReceiver"; import { ApiError, - ApiReceiver, ReceiverDetails, ReceiverDetailsInitialState, RejectMessage, @@ -39,37 +38,6 @@ export const getReceiverDetailsAction = createAsyncThunk< }, ); -export const updateReceiverDetailsAction = createAsyncThunk< - string, - { receiverId: string; email: string; externalId: string }, - { rejectValue: RejectMessage; state: RootState } ->( - "receiverDetails/updateReceiverDetailsAction", - async ( - { receiverId, email, externalId }, - { rejectWithValue, getState, dispatch }, - ) => { - const { token } = getState().userAccount; - - try { - const profileInfo = await patchReceiverInfo(token, receiverId, { - email, - externalId, - }); - return profileInfo.message; - } catch (error: unknown) { - const err = error as ApiError; - const errorString = handleApiErrorString(err); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error updating profile info: ${errorString}`, - errorExtras: err?.extras, - }); - } - }, -); - export const retryInvitationSMSAction = createAsyncThunk< string, { receiverWalletId: string }, @@ -168,21 +136,6 @@ const receiverDetailsSlice = createSlice({ state.status = "ERROR"; state.errorString = action.payload?.errorString; }); - //updateReceiverDetailsAction - builder.addCase( - updateReceiverDetailsAction.pending, - (state = initialState) => { - state.updateStatus = "PENDING"; - }, - ); - builder.addCase(updateReceiverDetailsAction.fulfilled, (state) => { - state.updateStatus = "SUCCESS"; - state.errorString = undefined; - }); - builder.addCase(updateReceiverDetailsAction.rejected, (state, action) => { - state.updateStatus = "ERROR"; - state.errorString = action.payload?.errorString; - }); //retryInvitationSMSAction builder.addCase( retryInvitationSMSAction.pending, @@ -206,37 +159,3 @@ export const receiverDetailsSelector = (state: RootState) => export const { reducer } = receiverDetailsSlice; export const { resetReceiverDetailsAction, resetRetryStatusAction } = receiverDetailsSlice.actions; - -const formatReceiver = (receiver: ApiReceiver): ReceiverDetails => ({ - id: receiver.id, - phoneNumber: receiver.phone_number, - email: receiver.email, - orgId: receiver.external_id, - // TODO: how to handle multiple - assetCode: receiver.received_amounts?.[0].asset_code, - totalReceived: receiver.received_amounts?.[0].received_amount, - stats: { - paymentsTotalCount: Number(receiver.total_payments), - paymentsSuccessfulCount: Number(receiver.successful_payments), - paymentsFailedCount: Number(receiver.failed_payments), - paymentsRemainingCount: Number(receiver.remaining_payments), - }, - wallets: receiver.wallets.map((w) => ({ - id: w.id, - stellarAddress: w.stellar_address, - provider: w.wallet.name, - invitedAt: w.invited_at, - createdAt: w.created_at, - smsLastSentAt: w.last_sms_sent, - totalPaymentsCount: Number(w.payments_received), - // TODO: how to handle multiple - assetCode: w.received_amounts[0].asset_code, - totalAmountReceived: w.received_amounts[0].received_amount, - // TODO: withdrawn amount - withdrawnAmount: "", - })), - verifications: receiver.verifications.map((v) => ({ - verificationField: v.VerificationField, - value: v.HashedValue, - })), -}); diff --git a/src/styles.scss b/src/styles.scss index eee8a4e..6a0e070 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -475,6 +475,10 @@ body { align-items: center; justify-content: flex-end; gap: pxToRem(8px); + + &--spaceBetween { + justify-content: space-between; + } } &__dropdownMenu {