diff --git a/src/api/deleteAsset.ts b/src/api/deleteAsset.ts deleted file mode 100644 index c9c01d3..0000000 --- a/src/api/deleteAsset.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { ApiAsset } from "types"; - -export const deleteAsset = async ( - token: string, - assetId: string, -): Promise => { - const response = await fetch(`${API_URL}/assets/${assetId}`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - return handleApiResponse(response); -}; diff --git a/src/api/getAssets.ts b/src/api/getAssets.ts deleted file mode 100644 index b225953..0000000 --- a/src/api/getAssets.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { ApiAsset } from "types"; - -export const getAssets = async (token: string): Promise => { - const response = await fetch(`${API_URL}/assets`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - return handleApiResponse(response); -}; diff --git a/src/api/postAssets.ts b/src/api/postAssets.ts deleted file mode 100644 index fbace86..0000000 --- a/src/api/postAssets.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { handleApiResponse } from "api/handleApiResponse"; -import { API_URL } from "constants/settings"; -import { ApiAsset } from "types"; - -export const postAssets = async ( - token: string, - asset: { - code: string; - issuer: string; - }, -): Promise => { - const response = await fetch(`${API_URL}/assets`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - code: asset.code, - issuer: asset.issuer, - }), - }); - - return handleApiResponse(response); -}; diff --git a/src/apiQueries/useAssetsAdd.ts b/src/apiQueries/useAssetsAdd.ts new file mode 100644 index 0000000..2d10c18 --- /dev/null +++ b/src/apiQueries/useAssetsAdd.ts @@ -0,0 +1,42 @@ +import { useMutation } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiAsset, AppError } from "types"; + +type Asset = { + assetCode: string; + assetIssuer: string; +}; + +export const useAssetsAdd = ({ + onSuccess, +}: { + onSuccess: (addedAsset: ApiAsset) => void; +}) => { + const mutation = useMutation({ + mutationFn: ({ assetCode, assetIssuer }: Asset) => { + return fetchApi(`${API_URL}/assets`, { + method: "POST", + body: JSON.stringify({ + code: assetCode, + issuer: assetIssuer, + }), + }); + }, + cacheTime: 0, + onSuccess, + }); + + return { + ...mutation, + error: mutation.error as AppError, + data: mutation.data as ApiAsset, + mutateAsync: async ({ assetCode, assetIssuer }: Asset) => { + try { + await mutation.mutateAsync({ assetCode, assetIssuer }); + } catch (e) { + // do nothing + } + }, + }; +}; diff --git a/src/apiQueries/useAssetsByWallet.ts b/src/apiQueries/useAssetsByWallet.ts new file mode 100644 index 0000000..31baa45 --- /dev/null +++ b/src/apiQueries/useAssetsByWallet.ts @@ -0,0 +1,20 @@ +import { useQuery } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiAsset, AppError } from "types"; + +export const useAssetsByWallet = (walletId: string) => { + const query = useQuery({ + queryKey: ["assets", "wallet", walletId], + queryFn: async () => { + if (!walletId) { + return; + } + + return await fetchApi(`${API_URL}/assets?wallet=${walletId}`); + }, + enabled: Boolean(walletId), + }); + + return query; +}; diff --git a/src/apiQueries/useAssetsDelete.ts b/src/apiQueries/useAssetsDelete.ts new file mode 100644 index 0000000..bffa613 --- /dev/null +++ b/src/apiQueries/useAssetsDelete.ts @@ -0,0 +1,33 @@ +import { useMutation } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiAsset, AppError } from "types"; + +export const useAssetsDelete = ({ + onSuccess, +}: { + onSuccess: (deletedAsset: ApiAsset) => void; +}) => { + const mutation = useMutation({ + mutationFn: (assetId: string) => { + return fetchApi(`${API_URL}/assets/${assetId}`, { + method: "DELETE", + }); + }, + cacheTime: 0, + onSuccess, + }); + + return { + ...mutation, + error: mutation.error as AppError, + data: mutation.data as ApiAsset, + mutateAsync: async (assetId: string) => { + try { + await mutation.mutateAsync(assetId); + } catch (e) { + // do nothing + } + }, + }; +}; diff --git a/src/apiQueries/useBalanceTrustline.ts b/src/apiQueries/useBalanceTrustline.ts new file mode 100644 index 0000000..c39af4c --- /dev/null +++ b/src/apiQueries/useBalanceTrustline.ts @@ -0,0 +1,42 @@ +import { useQuery } from "@tanstack/react-query"; +import { API_URL } from "constants/settings"; +import { fetchApi } from "helpers/fetchApi"; +import { ApiAsset, AppError, StellarAccountBalance } from "types"; + +type Trustline = { + id: string | null; + code: string; + issuer: string; + balance: string; + isNative: boolean; +}; + +export const useBalanceTrustline = ( + balances?: StellarAccountBalance[] | undefined, +) => { + const query = useQuery({ + queryKey: ["trustlines", { balances }], + queryFn: async () => { + const response = await fetchApi(`${API_URL}/assets`); + + return balances?.map((b) => { + const id = + response?.find( + (a: ApiAsset) => + a.code === b?.assetCode && a.issuer === b?.assetIssuer, + )?.id || null; + + return { + id, + code: b?.assetCode || "XLM", + issuer: b?.assetIssuer || "native", + balance: b.balance, + isNative: Boolean(!b.assetCode && !b.assetIssuer), + }; + }); + }, + enabled: Boolean(balances), + }); + + return query; +}; diff --git a/src/components/DisbursementDetails/index.tsx b/src/components/DisbursementDetails/index.tsx index 15c08db..15c9ec1 100644 --- a/src/components/DisbursementDetails/index.tsx +++ b/src/components/DisbursementDetails/index.tsx @@ -10,9 +10,9 @@ import { useDispatch } from "react-redux"; import { AppDispatch } from "store"; import { getCountriesAction } from "store/ducks/countries"; -import { getAssetsByWalletAction } from "store/ducks/assets"; import { useWallets } from "apiQueries/useWallets"; +import { useAssetsByWallet } from "apiQueries/useAssetsByWallet"; import { InfoTooltip } from "components/InfoTooltip"; import { formatUploadedFileDisplayName } from "helpers/formatUploadedFileDisplayName"; import { useRedux } from "hooks/useRedux"; @@ -61,7 +61,7 @@ export const DisbursementDetails: React.FC = ({ onChange, onValidate, }: DisbursementDetailsProps) => { - const { assets, countries } = useRedux("assets", "countries"); + const { countries } = useRedux("countries"); enum FieldId { NAME = "name", @@ -76,6 +76,12 @@ export const DisbursementDetails: React.FC = ({ isLoading: isWalletsLoading, } = useWallets(); + const { + data: walletAssets, + error: walletError, + isFetching: isWalletAssetsFetching, + } = useAssetsByWallet(details.wallet.id); + const dispatch: AppDispatch = useDispatch(); // Don't fetch again if we already have them in store @@ -88,7 +94,7 @@ export const DisbursementDetails: React.FC = ({ const apiErrors = [ countries.errorString, walletsError?.message, - assets.errorString, + walletError?.message, ]; const sanitizedApiErrors = apiErrors.filter((e) => Boolean(e)); @@ -157,12 +163,11 @@ export const DisbursementDetails: React.FC = ({ name: wallet?.name || "", }, }); - dispatch(getAssetsByWalletAction({ walletId: wallet?.id || "" })); break; case FieldId.ASSET_CODE: // eslint-disable-next-line no-case-declarations - const asset = assets.items.find((a: ApiAsset) => a.id === value); + const asset = walletAssets?.find((a: ApiAsset) => a.id === value); updateState({ asset: { @@ -274,10 +279,10 @@ export const DisbursementDetails: React.FC = ({ fieldSize="sm" onChange={updateDraftDetails} value={details.asset.id} - disabled={assets.status === "PENDING" || !details.wallet.id} + disabled={isWalletAssetsFetching || !details.wallet.id} > - {renderDropdownDefault(assets.status === "PENDING")} - {assets.items.map((asset: ApiAsset) => ( + {renderDropdownDefault(isWalletAssetsFetching)} + {walletAssets?.map((asset: ApiAsset) => ( diff --git a/src/components/WalletTrustlines/index.tsx b/src/components/WalletTrustlines/index.tsx index 77e258a..a66b89e 100644 --- a/src/components/WalletTrustlines/index.tsx +++ b/src/components/WalletTrustlines/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Button, Card, @@ -6,18 +6,17 @@ import { Modal, Notification, } from "@stellar/design-system"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { getAssets } from "api/getAssets"; -import { postAssets } from "api/postAssets"; -import { deleteAsset } from "api/deleteAsset"; import { InfoTooltip } from "components/InfoTooltip"; import { DropdownMenu } from "components/DropdownMenu"; import { MoreMenuButton } from "components/MoreMenuButton"; import { NotificationWithButtons } from "components/NotificationWithButtons"; +import { useBalanceTrustline } from "apiQueries/useBalanceTrustline"; +import { useAssetsAdd } from "apiQueries/useAssetsAdd"; +import { useAssetsDelete } from "apiQueries/useAssetsDelete"; import { parseApiError } from "helpers/parseApiError"; -import { useSessionToken } from "hooks/useSessionToken"; + import { ApiError, StellarAccountBalance } from "types"; import "./styles.scss"; @@ -41,96 +40,40 @@ export const WalletTrustlines = ({ fassetissuer: "", }; - type Trustline = { - id: string | null; - code: string; - issuer: string; - balance: string; - isNative: boolean; - }; - const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [isRemoveModalVisible, setIsRemoveModalVisible] = useState(false); const [formItems, setFormItems] = useState(initForm); const [formError, setFormError] = useState([]); const [removeAssetId, setRemoveAssetId] = useState(); - const [trustlines, setTrustlines] = useState(); const [successNotification, setSuccessNotification] = useState< { title: string; message: string } | undefined >(); - const sessionToken = useSessionToken(); - const ASSET_NAME: { [key: string]: string } = { XLM: "Stellar Lumens", USDC: "USD Coin", - EUROC: "EURO Coin", + EURC: "EURC", // SRT is used for testing SRT: "Stellar Reference Token", }; - const getTrustlines = async () => { - if (!balances) { - return []; - } - - return await getAssets(sessionToken); - }; - - const submitAddTrustline = () => { - return postAssets(sessionToken, { - code: formItems.fassetcode!, - issuer: formItems.fassetissuer!, - }); - }; - const { - isFetching: assetsIsFetching, - data: assets, - isError: assetsIsError, - isSuccess: assetsIsSuccess, - error: assetsError, - refetch: assetsRefetch, - } = useQuery({ - queryKey: ["WalletTrustlinesAssets"], - queryFn: () => (balances ? getTrustlines() : []), - enabled: Boolean(balances), - }); - - useEffect(() => { - if (balances?.length && assetsIsSuccess) { - const test = balances?.map((b) => { - const id = - assets?.find( - (a) => a.code === b?.assetCode && a.issuer === b?.assetIssuer, - )?.id || null; - - return { - id, - code: b?.assetCode || "XLM", - issuer: b?.assetIssuer || "native", - balance: b.balance, - isNative: Boolean(!b.assetCode && !b.assetIssuer), - }; - }); - - setTrustlines(test); - } - }, [assets, assetsIsSuccess, balances]); + isFetching: isTrustlinesFetching, + isLoading: isTrustlinesLoading, + data: trustlines, + error: trustlinesError, + } = useBalanceTrustline(balances); const { - mutate: trustlineAdd, - isLoading: trustlineAddIsLoading, - isError: trustlineAddIsError, + isLoading: isTrustlineAddLoading, + isError: isTrustlineAddError, error: trustlineAddError, + mutateAsync: trustlineAdd, reset: trustlineAddReset, - } = useMutation({ - mutationFn: submitAddTrustline, - retry: false, + } = useAssetsAdd({ onSuccess: (addedAsset) => { handleCloseModal(); onSuccess(); - assetsRefetch(); setSuccessNotification({ title: "Trustline added", message: `Trustline ${ASSET_NAME[addedAsset.code]} (${ @@ -141,18 +84,15 @@ export const WalletTrustlines = ({ }); const { - mutate: trustlineRemove, + mutateAsync: trustlineRemove, isLoading: trustlineRemoveIsLoading, isError: trustlineRemoveIsError, error: trustlineRemoveError, reset: trustlineRemoveReset, - } = useMutation({ - mutationFn: () => deleteAsset(sessionToken, removeAssetId!), - retry: false, + } = useAssetsDelete({ onSuccess: (removeAsset) => { handleCloseModal(); onSuccess(); - assetsRefetch(); setSuccessNotification({ title: "Trustline removed", message: `Trustline ${ASSET_NAME[removeAsset.code]} (${ @@ -169,7 +109,7 @@ export const WalletTrustlines = ({ setFormError([]); setRemoveAssetId(undefined); - if (trustlineAddIsError) { + if (isTrustlineAddError) { trustlineAddReset(); } @@ -197,7 +137,7 @@ export const WalletTrustlines = ({ [event.target.id]: event.target.value, }); - if (trustlineAddIsError) { + if (isTrustlineAddError) { trustlineAddReset(); } }; @@ -219,7 +159,7 @@ export const WalletTrustlines = ({ }; const renderContent = () => { - if (assetsIsFetching) { + if (isTrustlinesLoading || isTrustlinesFetching) { return
Loading…
; } @@ -239,31 +179,33 @@ export const WalletTrustlines = ({ >
{ASSET_NAME?.[a.code] &&
{ASSET_NAME?.[a.code]}
} - + {a.code}:{a.issuer}
{!a.isNative && a.id ? ( - }> - { - if (isRemoveEnabled) { - setIsRemoveModalVisible(true); - setRemoveAssetId(a.id!); +
+ }> + { + if (isRemoveEnabled) { + setIsRemoveModalVisible(true); + setRemoveAssetId(a.id!); + } + }} + isHighlight + aria-disabled={!isRemoveEnabled} + title={ + !isRemoveEnabled + ? "You can only remove an asset when the asset balance is 0" + : "" } - }} - isHighlight - aria-disabled={!isRemoveEnabled} - title={ - !isRemoveEnabled - ? "You can only remove an asset when the asset balance is 0" - : "" - } - > - Remove trustline - - + > + Remove trustline + + +
) : null} ); @@ -285,7 +227,7 @@ export const WalletTrustlines = ({ }; const getRemoveAssetConfirmation = () => { - const asset = assets?.find((a) => a.id === removeAssetId); + const asset = trustlines?.find((a) => a.id === removeAssetId); if (asset) { return `Are you sure you want to remove ${ @@ -298,9 +240,9 @@ export const WalletTrustlines = ({ return ( <> - {assetsIsError ? ( + {trustlinesError ? ( - {assetsError as string} + {trustlinesError.message} ) : null} @@ -341,12 +283,18 @@ export const WalletTrustlines = ({
{ event.preventDefault(); - trustlineAdd(); + + if (formItems.fassetcode && formItems.fassetissuer) { + trustlineAdd({ + assetCode: formItems.fassetcode, + assetIssuer: formItems.fassetissuer, + }); + } }} onReset={handleCloseModal} > - {trustlineAddIsError ? ( + {isTrustlineAddError ? ( {parseApiError(trustlineAddError as ApiError)} @@ -388,7 +336,7 @@ export const WalletTrustlines = ({ size="sm" variant="secondary" type="reset" - isLoading={trustlineAddIsLoading} + isLoading={isTrustlineAddLoading} > Cancel @@ -397,7 +345,7 @@ export const WalletTrustlines = ({ variant="primary" type="submit" disabled={!canSubmit} - isLoading={trustlineAddIsLoading} + isLoading={isTrustlineAddLoading} > Add trustline @@ -414,7 +362,10 @@ export const WalletTrustlines = ({ { event.preventDefault(); - trustlineRemove(); + + if (removeAssetId) { + trustlineRemove(removeAssetId); + } }} onReset={handleCloseModal} > diff --git a/src/components/WalletTrustlines/styles.scss b/src/components/WalletTrustlines/styles.scss index 595d6d5..711c2dd 100644 --- a/src/components/WalletTrustlines/styles.scss +++ b/src/components/WalletTrustlines/styles.scss @@ -4,7 +4,6 @@ &__asset { display: flex; justify-content: space-between; - gap: pxToRem(16px); border: 1px solid var(--color-gray-30); border-radius: pxToRem(4px); background-color: var(--color-gray-00); @@ -13,17 +12,26 @@ &__info { display: flex; flex-direction: column; - gap: pxToRem(8px); + gap: pxToRem(4px); font-size: pxToRem(14px); line-height: pxToRem(22px); font-weight: var(--font-weight-regular); color: var(--color-gray-80); + overflow: hidden; span { color: var(--color-gray-60); font-size: pxToRem(12px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } + + &__button { + flex-shrink: 0; + padding-left: pxToRem(8px); + } } &__button { diff --git a/src/pages/DistributionAccount.tsx b/src/pages/DistributionAccount.tsx index 423b0ce..0ccd09d 100644 --- a/src/pages/DistributionAccount.tsx +++ b/src/pages/DistributionAccount.tsx @@ -7,10 +7,13 @@ import { Notification, } from "@stellar/design-system"; import { useDispatch } from "react-redux"; + import { InfoTooltip } from "components/InfoTooltip"; import { SectionHeader } from "components/SectionHeader"; import { AccountBalances } from "components/AccountBalances"; import { WalletTrustlines } from "components/WalletTrustlines"; +import { LoadingContent } from "components/LoadingContent"; + import { useRedux } from "hooks/useRedux"; import { useOrgAccountInfo } from "hooks/useOrgAccountInfo"; import { AppDispatch } from "store"; @@ -25,6 +28,10 @@ export const DistributionAccount = () => { const dispatch: AppDispatch = useDispatch(); const renderContent = () => { + if (organization.status === "PENDING") { + return ; + } + if (organization.errorString) { return ( @@ -33,7 +40,7 @@ export const DistributionAccount = () => { ); } - if (!assetBalances || assetBalances.length === 0) { + if (assetBalances?.length === 0) { return
There are no distribution accounts
; } @@ -62,7 +69,7 @@ export const DistributionAccount = () => { Current balance: <> - {assetBalances.map((a) => ( + {assetBalances?.map((a) => ( {} diff --git a/src/store/ducks/assets.ts b/src/store/ducks/assets.ts deleted file mode 100644 index ce5b594..0000000 --- a/src/store/ducks/assets.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { RootState } from "store"; -import { getAssets } from "api/getAssets"; -import { getAssetsByWallet } from "api/getAssetsByWallet"; -import { handleApiErrorString } from "api/handleApiErrorString"; -import { endSessionIfTokenInvalid } from "helpers/endSessionIfTokenInvalid"; -import { ApiAsset, ApiError, AssetsInitialState, RejectMessage } from "types"; - -export const getAssetsAction = createAsyncThunk< - ApiAsset[], - undefined, - { rejectValue: RejectMessage; state: RootState } ->( - "assets/getAssetsAction", - async (_, { rejectWithValue, getState, dispatch }) => { - const { token } = getState().userAccount; - - try { - const assets = await getAssets(token); - // 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}`, - }); - } - }, -); - -export const getAssetsByWalletAction = createAsyncThunk< - ApiAsset[], - { walletId: string }, - { rejectValue: RejectMessage; state: RootState } ->( - "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, - errorString: undefined, -}; - -const assetsSlice = createSlice({ - name: "assets", - initialState, - reducers: {}, - extraReducers: (builder) => { - builder.addCase(getAssetsAction.pending, (state = initialState) => { - state.status = "PENDING"; - }); - builder.addCase(getAssetsAction.fulfilled, (state, action) => { - state.items = action.payload; - state.status = "SUCCESS"; - state.errorString = undefined; - }); - builder.addCase(getAssetsAction.rejected, (state, action) => { - 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; - }); - }, -}); - -export const assetsSelector = (state: RootState) => state.assets; -export const { reducer } = assetsSlice; diff --git a/src/store/index.ts b/src/store/index.ts index ca1b94f..9a62def 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -9,7 +9,6 @@ import BigNumber from "bignumber.js"; import { RESET_STORE_ACTION_TYPE } from "constants/settings"; -import { reducer as assets } from "store/ducks/assets"; import { reducer as countries } from "store/ducks/countries"; import { reducer as disbursementDetails } from "store/ducks/disbursementDetails"; import { reducer as disbursementDrafts } from "store/ducks/disbursementDrafts"; @@ -33,7 +32,6 @@ const isSerializable = (value: any) => BigNumber.isBigNumber(value) || isPlain(value); const reducers = combineReducers({ - assets, countries, disbursementDetails, disbursementDrafts, diff --git a/src/types/index.ts b/src/types/index.ts index 68b454e..4e4f80d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -66,12 +66,6 @@ export type CountriesInitialState = { errorString?: string; }; -export type AssetsInitialState = { - items: ApiAsset[]; - status: ActionStatus | undefined; - errorString?: string; -}; - export type DisbursementDraftsInitialState = { items: DisbursementDraft[]; status: ActionStatus | undefined; @@ -125,7 +119,6 @@ export type ProfileInitialState = { }; export interface Store { - assets: AssetsInitialState; countries: CountriesInitialState; disbursementDetails: DisbursementDetailsInitialState; disbursementDrafts: DisbursementDraftsInitialState;