diff --git a/src/api/getReceiverDetails.ts b/src/api/getReceiverDetails.ts deleted file mode 100644 index 8df40ff..0000000 --- a/src/api/getReceiverDetails.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { ApiReceiver } from "types"; - -export const getReceiverDetails = async ( - token: string, - receiverId: string, -): Promise => { - const response = await fetch(`${API_URL}/receivers/${receiverId}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - return handleApiResponse(response); -}; diff --git a/src/api/retryInvitationSMS.ts b/src/api/retryInvitationSMS.ts deleted file mode 100644 index 8a385a8..0000000 --- a/src/api/retryInvitationSMS.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; - -export const retryInvitationSMS = async ( - token: string, - receiverWalletId: string, -): Promise<{ message: string }> => { - const response = await fetch( - `${API_URL}/receivers/wallets/${receiverWalletId}`, - { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - }, - }, - ); - - return handleApiResponse(response); -}; diff --git a/src/apiQueries/useReceiverWalletInviteSmsRetry.ts b/src/apiQueries/useReceiverWalletInviteSmsRetry.ts new file mode 100644 index 0000000..c69d453 --- /dev/null +++ b/src/apiQueries/useReceiverWalletInviteSmsRetry.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { AppError } from "types"; + +export const useReceiverWalletInviteSmsRetry = ( + receiverWalletId: string | undefined, +) => { + const query = useQuery<{ message: string }, AppError>({ + queryKey: ["receivers", "wallets", "sms", "retry", receiverWalletId], + queryFn: async () => { + return await fetchApi( + `${API_URL}/receivers/wallets/${receiverWalletId}`, + { + method: "PATCH", + }, + ); + }, + // Don't fire the query on mount + enabled: false, + }); + + return query; +}; diff --git a/src/components/ReceiverPayments.tsx b/src/components/ReceiverPayments.tsx new file mode 100644 index 0000000..a410c16 --- /dev/null +++ b/src/components/ReceiverPayments.tsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { Heading, Select } from "@stellar/design-system"; + +import { AppDispatch } from "store"; +import { + getReceiverPaymentsAction, + getReceiverPaymentsWithParamsAction, +} from "store/ducks/receiverPayments"; +import { useRedux } from "hooks/useRedux"; +import { PAGE_LIMIT_OPTIONS } from "constants/settings"; + +import { SectionHeader } from "components/SectionHeader"; +import { PaymentsTable } from "components/PaymentsTable"; +import { Pagination } from "components/Pagination"; +import { renderTextWithCount } from "helpers/renderTextWithCount"; + +export const ReceiverPayments = ({ receiverId }: { receiverId: string }) => { + const { receiverPayments } = useRedux("receiverPayments"); + + const [currentPage, setCurrentPage] = useState(1); + const [pageLimit, setPageLimit] = useState(20); + + const dispatch: AppDispatch = useDispatch(); + + const maxPages = receiverPayments.pagination?.pages || 1; + + useEffect(() => { + if (receiverId) { + dispatch(getReceiverPaymentsAction(receiverId)); + } + }, [receiverId, dispatch]); + + const handlePageLimitChange = ( + event: React.ChangeEvent, + ) => { + event.preventDefault(); + + const pageLimit = Number(event.target.value); + setPageLimit(pageLimit); + setCurrentPage(1); + + if (receiverId) { + // Need to make sure we'll be loading page 1 + dispatch( + getReceiverPaymentsWithParamsAction({ + receiver_id: receiverId, + page_limit: pageLimit.toString(), + page: "1", + }), + ); + } + }; + + return ( +
+ + + + + {renderTextWithCount( + receiverPayments.pagination?.total || 0, + "Payment", + "Payments", + )} + + + + +
+ +
+ + { + setCurrentPage(page); + + if (receiverId) { + dispatch( + getReceiverPaymentsWithParamsAction({ + receiver_id: receiverId, + page: page.toString(), + }), + ); + } + }} + isLoading={receiverPayments.status === "PENDING"} + /> +
+
+
+ + +
+ ); +}; diff --git a/src/helpers/renderTextWithCount.ts b/src/helpers/renderTextWithCount.ts new file mode 100644 index 0000000..1dc39aa --- /dev/null +++ b/src/helpers/renderTextWithCount.ts @@ -0,0 +1,15 @@ +import { number } from "helpers/formatIntlNumber"; + +export const renderTextWithCount = ( + itemCount: number, + singularText: string, + pluralText: string, +) => { + if (itemCount === 1) { + return `1 ${singularText}`; + } else if (itemCount > 1) { + return `${number.format(itemCount)} ${pluralText}`; + } + + return pluralText; +}; diff --git a/src/pages/ReceiverDetails.tsx b/src/pages/ReceiverDetails.tsx index b545cf9..a5f2c84 100644 --- a/src/pages/ReceiverDetails.tsx +++ b/src/pages/ReceiverDetails.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; -import { useDispatch } from "react-redux"; +import { useQueryClient } from "@tanstack/react-query"; import { Card, Heading, @@ -10,65 +10,94 @@ import { Button, } from "@stellar/design-system"; -import { AppDispatch } from "store"; -import { - getReceiverDetailsAction, - resetRetryStatusAction, - retryInvitationSMSAction, -} from "store/ducks/receiverDetails"; -import { - getReceiverPaymentsAction, - getReceiverPaymentsWithParamsAction, -} from "store/ducks/receiverPayments"; -import { useRedux } from "hooks/useRedux"; -import { PAGE_LIMIT_OPTIONS, Routes } from "constants/settings"; +import { GENERIC_ERROR_MESSAGE, Routes } from "constants/settings"; import { Breadcrumbs } from "components/Breadcrumbs"; import { SectionHeader } from "components/SectionHeader"; import { CopyWithIcon } from "components/CopyWithIcon"; import { AssetAmount } from "components/AssetAmount"; import { InfoTooltip } from "components/InfoTooltip"; -import { PaymentsTable } from "components/PaymentsTable"; -import { Pagination } from "components/Pagination"; import { ReceiverWalletBalance } from "components/ReceiverWalletBalance"; import { ReceiverWalletHistory } from "components/ReceiverWalletHistory"; +import { LoadingContent } from "components/LoadingContent"; import { NotificationWithButtons } from "components/NotificationWithButtons"; +import { ReceiverPayments } from "components/ReceiverPayments"; -import { number, percent } from "helpers/formatIntlNumber"; +import { useReceiversReceiverId } from "apiQueries/useReceiversReceiverId"; +import { useReceiverWalletInviteSmsRetry } from "apiQueries/useReceiverWalletInviteSmsRetry"; + +import { percent } from "helpers/formatIntlNumber"; import { renderNumberOrDash } from "helpers/renderNumberOrDash"; import { formatDateTime } from "helpers/formatIntlDateTime"; import { shortenAccountKey } from "helpers/shortenAccountKey"; -import { ReceiverWallet } from "types"; +import { renderTextWithCount } from "helpers/renderTextWithCount"; + +import { ReceiverWallet, ReceiverDetails as ReceiverDetailsType } from "types"; export const ReceiverDetails = () => { const { id: receiverId } = useParams(); - const { receiverDetails, receiverPayments } = useRedux( - "receiverDetails", - "receiverPayments", - ); - - const [currentPage, setCurrentPage] = useState(1); - const [pageLimit, setPageLimit] = useState(20); const [selectedWallet, setSelectedWallet] = useState(); - const dispatch: AppDispatch = useDispatch(); + const { + data: receiverDetails, + isSuccess: isReceiverDetailsSuccess, + isLoading: isReceiverDetailsLoading, + error: receiverDetailsError, + } = useReceiversReceiverId({ + receiverId, + dataFormat: "receiver", + }); + + const { + isSuccess: isSmsRetrySuccess, + isFetching: isSmsRetryFetching, + isError: isSmsRetryError, + error: smsRetryError, + refetch: retrySmsInvite, + } = useReceiverWalletInviteSmsRetry(selectedWallet?.id); + + const [selectedWalletId, setSelectedWalletId] = useState( + receiverDetails?.wallets?.[0]?.id, + ); + + const queryClient = useQueryClient(); const navigate = useNavigate(); - const { stats } = receiverDetails; - const maxPages = receiverPayments.pagination?.pages || 1; - const defaultWallet = receiverDetails.wallets?.[0]; + const stats = receiverDetails?.stats; + const defaultWalletId = receiverDetails?.wallets?.[0]?.id; + + const resetSmsRetry = () => { + queryClient.resetQueries({ + queryKey: ["receivers", "wallets", "sms", "retry"], + }); + }; useEffect(() => { - if (receiverId) { - dispatch(getReceiverDetailsAction(receiverId)); - dispatch(getReceiverPaymentsAction(receiverId)); + if (isReceiverDetailsSuccess) { + setSelectedWalletId(defaultWalletId); } - }, [receiverId, dispatch]); + }, [defaultWalletId, isReceiverDetailsSuccess]); useEffect(() => { - setSelectedWallet(defaultWallet); - }, [defaultWallet]); + if (selectedWalletId) { + setSelectedWallet( + receiverDetails?.wallets.find((w) => w.id === selectedWalletId), + ); + } + // We don't want to track receiverDetails here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedWalletId]); + + useEffect(() => { + return () => { + if (isSmsRetrySuccess || isSmsRetryError) { + resetSmsRetry(); + } + }; + // Don't need to include queryClient.resetQueries + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSmsRetryError, isSmsRetrySuccess]); const calculateRate = () => { if (stats?.paymentsSuccessfulCount && stats?.paymentsTotalCount) { @@ -78,31 +107,6 @@ export const ReceiverDetails = () => { return 0; }; - const handlePageLimitChange = ( - event: React.ChangeEvent, - ) => { - event.preventDefault(); - - const pageLimit = Number(event.target.value); - setPageLimit(pageLimit); - setCurrentPage(1); - - if (receiverId) { - // Need to make sure we'll be loading page 1 - dispatch( - getReceiverPaymentsWithParamsAction({ - receiver_id: receiverId, - page_limit: pageLimit.toString(), - page: "1", - }), - ); - } - }; - - const handleRetryInvitation = (receiverWalletId: string) => { - dispatch(retryInvitationSMSAction({ receiverWalletId })); - }; - const setCardTemplateRows = (rows: number) => { return { "--StatCard-template-rows": rows, @@ -110,6 +114,10 @@ export const ReceiverDetails = () => { }; const renderInfoCards = () => { + if (!receiverDetails) { + return null; + } + return (
@@ -209,20 +217,6 @@ export const ReceiverDetails = () => { ); }; - const renderTitle = ( - itemCount: number, - singularText: string, - pluralText: string, - ) => { - if (itemCount === 1) { - return `1 ${singularText}`; - } else if (itemCount > 1) { - return `${number.format(itemCount)} ${pluralText}`; - } - - return pluralText; - }; - const renderWalletOptionText = (wallet: ReceiverWallet) => { return `${wallet.provider} (${ wallet.stellarAddress @@ -232,9 +226,13 @@ export const ReceiverDetails = () => { }; const renderWallets = () => { + if (!receiverDetails) { + return null; + } + return (
- {receiverDetails.retryInvitationStatus === "SUCCESS" && ( + {isSmsRetrySuccess && ( { { label: "Dismiss", onClick: () => { - dispatch(resetRetryStatusAction()); + resetSmsRetry(); }, }, ]} @@ -250,7 +248,7 @@ export const ReceiverDetails = () => { {" "} )} - {receiverDetails.retryInvitationStatus === "ERROR" && ( + {smsRetryError && ( { { label: "Dismiss", onClick: () => { - dispatch(resetRetryStatusAction()); + resetSmsRetry(); }, }, ]} > - {receiverDetails.errorString} + {smsRetryError.message} )}
@@ -271,13 +269,9 @@ export const ReceiverDetails = () => {
- {renderTitle(receiverDetails.wallets.length, "wallet", "wallets")} + {renderTextWithCount( + receiverDetails.wallets.length, + "wallet", + "wallets", + )}
@@ -296,13 +294,17 @@ export const ReceiverDetails = () => {
@@ -448,13 +450,14 @@ export const ReceiverDetails = () => { }; const renderContent = () => { - if ( - receiverDetails.errorString && - receiverDetails.retryInvitationStatus !== "ERROR" - ) { + if (isReceiverDetailsLoading) { + return ; + } + + if (receiverDetailsError || !receiverDetails) { return ( - {receiverDetails.errorString} +
{receiverDetailsError?.message || GENERIC_ERROR_MESSAGE}
); } @@ -509,61 +512,7 @@ export const ReceiverDetails = () => { {renderWallets()} -
- - - - - {renderTitle( - receiverPayments.pagination?.total || 0, - "Payment", - "Payments", - )} - - - - -
- -
- - { - setCurrentPage(page); - - if (receiverId) { - dispatch( - getReceiverPaymentsWithParamsAction({ - receiver_id: receiverId, - page: page.toString(), - }), - ); - } - }} - isLoading={receiverPayments.status === "PENDING"} - /> -
-
-
- - -
+ ); }; diff --git a/src/pages/ReceiverDetailsEdit.tsx b/src/pages/ReceiverDetailsEdit.tsx index 902ebbe..fe31d8f 100644 --- a/src/pages/ReceiverDetailsEdit.tsx +++ b/src/pages/ReceiverDetailsEdit.tsx @@ -184,8 +184,9 @@ export const ReceiverDetailsEdit = () => {
- {/* TODO: info text */} - Receiver info + + Receiver info +
diff --git a/src/store/ducks/receiverDetails.ts b/src/store/ducks/receiverDetails.ts deleted file mode 100644 index c7135a7..0000000 --- a/src/store/ducks/receiverDetails.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { RootState } from "store"; -import { getReceiverDetails } from "api/getReceiverDetails"; -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, - ReceiverDetails, - ReceiverDetailsInitialState, - RejectMessage, -} from "types"; - -export const getReceiverDetailsAction = createAsyncThunk< - ReceiverDetails, - string, - { rejectValue: RejectMessage; state: RootState } ->( - "receiverDetails/getReceiverDetailsAction", - async (receiverId, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const receiverDetails = await getReceiverDetails(token, receiverId); - refreshSessionToken(dispatch); - - return formatReceiver(receiverDetails); - } catch (error: unknown) { - const errorString = handleApiErrorString(error as ApiError); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error fetching receiver details: ${errorString}`, - }); - } - }, -); - -export const retryInvitationSMSAction = createAsyncThunk< - string, - { receiverWalletId: string }, - { rejectValue: RejectMessage; state: RootState } ->( - "receiverDetails/retryInvitationSMSAction", - async ({ receiverWalletId }, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const response = await retryInvitationSMS(token, receiverWalletId); - return response.message; - } catch (error: unknown) { - const err = error as ApiError; - const errorString = handleApiErrorString(err); - endSessionIfTokenInvalid(errorString, dispatch); - - return rejectWithValue({ - errorString: `Error retrying invitation: ${errorString}`, - errorExtras: err?.extras, - }); - } - }, -); - -const initialState: ReceiverDetailsInitialState = { - id: "", - phoneNumber: "", - email: "", - assetCode: undefined, - totalReceived: undefined, - orgId: "", - stats: { - paymentsTotalCount: 0, - paymentsSuccessfulCount: 0, - paymentsFailedCount: 0, - paymentsRemainingCount: 0, - }, - wallets: [ - { - id: "", - stellarAddress: "", - provider: "", - invitedAt: "", - createdAt: "", - smsLastSentAt: "", - totalPaymentsCount: 0, - totalAmountReceived: "", - withdrawnAmount: "", - assetCode: "", - }, - ], - verifications: [ - { - verificationField: "", - value: "", - }, - ], - status: undefined, - updateStatus: undefined, - retryInvitationStatus: undefined, - errorString: undefined, -}; - -const receiverDetailsSlice = createSlice({ - name: "receiverDetails", - initialState, - reducers: { - resetReceiverDetailsAction: () => initialState, - resetRetryStatusAction: (state) => { - state.retryInvitationStatus = undefined; - }, - }, - extraReducers: (builder) => { - // Get receiver details - builder.addCase( - getReceiverDetailsAction.pending, - (state = initialState) => { - state.status = "PENDING"; - }, - ); - builder.addCase(getReceiverDetailsAction.fulfilled, (state, action) => { - state.id = action.payload.id; - state.phoneNumber = action.payload.phoneNumber; - state.assetCode = action.payload.assetCode; - state.totalReceived = action.payload.totalReceived; - state.stats = action.payload.stats; - state.wallets = action.payload.wallets; - state.verifications = action.payload.verifications; - state.email = action.payload.email; - state.orgId = action.payload.orgId; - state.status = "SUCCESS"; - state.errorString = undefined; - }); - builder.addCase(getReceiverDetailsAction.rejected, (state, action) => { - state.status = "ERROR"; - state.errorString = action.payload?.errorString; - }); - //retryInvitationSMSAction - builder.addCase( - retryInvitationSMSAction.pending, - (state = initialState) => { - state.retryInvitationStatus = "PENDING"; - }, - ); - builder.addCase(retryInvitationSMSAction.fulfilled, (state) => { - state.retryInvitationStatus = "SUCCESS"; - state.errorString = undefined; - }); - builder.addCase(retryInvitationSMSAction.rejected, (state, action) => { - state.retryInvitationStatus = "ERROR"; - state.errorString = action.payload?.errorString; - }); - }, -}); - -export const receiverDetailsSelector = (state: RootState) => - state.receiverDetails; -export const { reducer } = receiverDetailsSlice; -export const { resetReceiverDetailsAction, resetRetryStatusAction } = - receiverDetailsSlice.actions; diff --git a/src/store/index.ts b/src/store/index.ts index 3012560..251476a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -17,7 +17,6 @@ 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 profile } from "store/ducks/profile"; -import { reducer as receiverDetails } from "store/ducks/receiverDetails"; import { reducer as receiverPayments } from "store/ducks/receiverPayments"; import { reducer as userAccount } from "store/ducks/userAccount"; import { reducer as users } from "store/ducks/users"; @@ -46,7 +45,6 @@ const reducers = combineReducers({ forgotPassword, organization, profile, - receiverDetails, receiverPayments, userAccount, users,