Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RateLine } from './components/RateLine';
import { SenderReceiverLine } from './components/SenderReceiverLine';
import { FeeLine } from './components/FeeLine';
import { ExplorerInfo } from './components/ExplorerInfo';
import { LocalActionView } from './components/LocalActionView';

const dateFormatter = new Intl.DateTimeFormat('en', {
year: 'numeric',
Expand Down Expand Up @@ -97,6 +98,11 @@ export function ActionDetailedView({
) : null}
</VStack>
) : null}
<LocalActionView
localActionKey={action.transaction.hash}
showTransferInfo={isFailed}
status={action.transaction.status}
/>
<Surface padding={16}>
<VStack gap={24}>
<ExplorerInfo action={action} networks={networks} />
Expand Down
137 changes: 137 additions & 0 deletions src/ui/pages/History/ActionDetailedView/components/LocalActionView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useMemo } from 'react';
import type { ActionTransfers } from 'defi-sdk';
import { useAssetsPrices } from 'defi-sdk';
import { useStore } from '@store-unit/react';
import type { SwapLocalAction } from 'src/ui/transactions/local-actions-store';
import {
localActionsStore,
LocalActionsStore,
type LocalAction,
} from 'src/ui/transactions/local-actions-store';
import { Button } from 'src/ui/ui-kit/Button';
import { UnstyledLink } from 'src/ui/ui-kit/UnstyledLink';
import { VStack } from 'src/ui/ui-kit/VStack';
import RetryIcon from 'jsx:src/ui/assets/actions/swap.svg';
import { HStack } from 'src/ui/ui-kit/HStack';
import { UIText } from 'src/ui/ui-kit/UIText';
import { createChain } from 'src/modules/networks/Chain';
import { useCurrency } from 'src/modules/currency/useCurrency';
import { commonToBase } from 'src/shared/units/convert';
import { getDecimals } from 'src/modules/networks/asset';
import type { ClientTransactionStatus } from 'src/modules/ethereum/transactions/addressAction';
import { TransferInfo } from './TransferInfo';

function SwapLocalActionView({
localAction,
showTransferInfo,
status,
}: {
localAction: SwapLocalAction;
showTransferInfo?: boolean;
status: ClientTransactionStatus;
}) {
const link = LocalActionsStore.getActionLink(localAction);
const chain = createChain(localAction.chain);
const { currency } = useCurrency();
const { value, isLoading } = useAssetsPrices({
asset_codes: [localAction.spendTokenId, localAction.receiveTokenId],
currency,
});

const transfers = useMemo<ActionTransfers>(() => {
const spendToken = value?.[localAction.spendTokenId];
const receiveToken = value?.[localAction.receiveTokenId];
return {
incoming: receiveToken
? [
{
asset: { fungible: receiveToken },
quantity: commonToBase(
localAction.receiveInput,
getDecimals({ asset: receiveToken, chain })
).toFixed(),
price: receiveToken.price?.value || null,
},
]
: [],
outgoing: spendToken
? [
{
asset: { fungible: spendToken },
quantity: commonToBase(
localAction.spendInput,
getDecimals({ asset: spendToken, chain })
).toFixed(),
price: spendToken.price?.value || null,
},
]
: [],
};
}, [value, chain, localAction]);

const isFailed = status === 'failed' || status === 'dropped';

if (isLoading) {
return null;
}

return (
<VStack gap={12}>
{showTransferInfo ? (
<VStack gap={4}>
{transfers.outgoing ? (
<TransferInfo
chain={chain}
direction="outgoing"
title="Send"
transfers={transfers.outgoing}
/>
) : null}
{transfers.incoming ? (
<TransferInfo
chain={chain}
direction="incoming"
title="Receive"
transfers={transfers.incoming}
/>
) : null}
</VStack>
) : null}
{link ? (
<Button kind="primary" as={UnstyledLink} to={link}>
<HStack gap={8} alignItems="center">
<RetryIcon />
<UIText kind="small/accent">
{isFailed ? 'Try Again' : 'Swap Again'}
</UIText>
</HStack>
</Button>
) : null}
</VStack>
);
}

export function LocalActionView({
localActionKey,
showTransferInfo,
status,
}: {
localActionKey: string;
showTransferInfo?: boolean;
status: ClientTransactionStatus;
}) {
const { localActions } = useStore(localActionsStore);

const localAction = localActions[localActionKey] as LocalAction | undefined;

if (localAction?.kind === 'swap') {
return (
<SwapLocalActionView
localAction={localAction}
status={status}
showTransferInfo={showTransferInfo}
/>
);
}
return null;
}
18 changes: 17 additions & 1 deletion src/ui/pages/SwapForm/SuccessState/SuccessState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ import { SuccessStateLoader } from 'src/ui/shared/forms/SuccessState/SuccessStat
import { SuccessStateToken } from 'src/ui/shared/forms/SuccessState/SuccessStateToken';
import { useActionStatusByHash } from 'src/ui/shared/forms/SuccessState/useActionStatusByHash';
import { NavigationTitle } from 'src/ui/components/NavigationTitle';
import { GasbackDecorated } from '../../SendForm/SuccessState/SuccessState';
import RetryIcon from 'jsx:src/ui/assets/actions/swap.svg';
import { Button } from 'src/ui/ui-kit/Button';
import { HStack } from 'src/ui/ui-kit/HStack';
import { UIText } from 'src/ui/ui-kit/UIText';
import type { BareAddressPosition } from '../BareAddressPosition';
import { GasbackDecorated } from '../../SendForm/SuccessState/SuccessState';

export function SuccessState({
swapFormState,
spendPosition,
receivePosition,
hash,
onRetry,
onDone,
gasbackValue,
}: {
Expand All @@ -26,6 +31,7 @@ export function SuccessState({
receivePosition: BareAddressPosition;
hash: string;
gasbackValue: number | null;
onRetry: () => void;
onDone: () => void;
}) {
const { networks } = useNetworks();
Expand Down Expand Up @@ -82,6 +88,16 @@ export function SuccessState({
<GasbackDecorated value={gasbackValue} />
) : null
}
secondaryAction={
actionStatus === 'failed' || actionStatus === 'dropped' ? (
<Button kind="primary" onClick={onRetry}>
<HStack gap={8} alignItems="center" justifyContent="center">
<RetryIcon />
<UIText kind="small/accent">Try Again</UIText>
</HStack>
</Button>
) : null
}
onDone={onDone}
/>
</>
Expand Down
46 changes: 44 additions & 2 deletions src/ui/pages/SwapForm/SwapForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import type { ZerionApiClient } from 'src/modules/zerion-api/zerion-api-bare';
import { useGasbackEstimation } from 'src/modules/ethereum/account-abstraction/rewards';
import { HiddenValidationInput } from 'src/ui/shared/forms/HiddenValidationInput';
import { getNetworksStore } from 'src/modules/networks/networks-store.client';
import { localActionsStore } from 'src/ui/transactions/local-actions-store';
import {
DEFAULT_CONFIGURATION,
applyConfiguration,
Expand Down Expand Up @@ -303,7 +304,10 @@ export function SwapFormComponent() {
: null;
const {
enough_allowance,
allowanceQuery: { refetch: refetchAllowanceQuery },
allowanceQuery: {
refetch: refetchAllowanceQuery,
remove: removeAllowanceQuery,
},
approvalTransactionQuery: { isFetching: approvalTransactionIsFetching },
approvalTransaction,
} = useApproveHandler({
Expand Down Expand Up @@ -447,8 +451,8 @@ export function SwapFormComponent() {
}, [
approveTxStatus,
refetchAllowanceQuery,
refetchNonce,
refetchQuotes,
refetchNonce,
resetApproveMutation,
]);

Expand Down Expand Up @@ -506,6 +510,35 @@ export function SwapFormComponent() {
onBeforeSubmit();
return 'sendTransaction';
},
onSuccess: (hash) => {
if (!snapshotRef.current) {
return;
}
const {
chainInput,
receiveInput,
spendInput,
receiveTokenInput,
spendTokenInput,
} = snapshotRef.current;
if (
!chainInput ||
!spendInput ||
!receiveInput ||
!spendTokenInput ||
!receiveTokenInput
) {
return;
}
localActionsStore.saveAction(hash, {
kind: 'swap',
chain: chainInput,
receiveInput,
spendInput,
spendTokenId: spendTokenInput,
receiveTokenId: receiveTokenInput,
});
},
});

const resetMutationIfNotLoading = useEvent(() => {
Expand Down Expand Up @@ -568,6 +601,15 @@ export function SwapFormComponent() {
receivePosition={receivePosition}
swapFormState={snapshotRef.current}
gasbackValue={gasbackValueRef.current}
onRetry={() => {
reset();
removeAllowanceQuery();
refetchNonce();
refetchQuotes();
snapshotRef.current = null;
feeValueCommonRef.current = null;
gasbackValueRef.current = null;
}}
onDone={() => {
reset();
snapshotRef.current = null;
Expand Down
11 changes: 8 additions & 3 deletions src/ui/shared/forms/SuccessState/SuccessStateLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export function SuccessStateLoader({
explorerUrl,
error,
confirmedContent,
secondaryAction,
onDone,
}: {
startItem: React.ReactNode;
Expand All @@ -193,6 +194,7 @@ export function SuccessStateLoader({
explorerUrl?: string;
error?: string;
confirmedContent?: React.ReactNode;
secondaryAction?: React.ReactNode;
onDone?: () => void;
}) {
const showLongWaitNotice = useRenderDelay(5000);
Expand Down Expand Up @@ -309,9 +311,12 @@ export function SuccessStateLoader({
</UIText>
) : null}
</VStack>
<Button kind="regular" style={{ width: '100%' }} onClick={onDone}>
Done
</Button>
<VStack gap={4}>
{secondaryAction}
<Button kind="regular" style={{ width: '100%' }} onClick={onDone}>
Done
</Button>
</VStack>
<Spacer height={24} />
</VStack>
</PageColumn>
Expand Down
71 changes: 71 additions & 0 deletions src/ui/transactions/local-actions-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PersistentStore } from 'src/modules/persistent-store';

type BaseLocalAction = {
datetime: number;
};

export type SendLocalAction = {
kind: 'send';
recepient: string;
Copy link

Copilot AI Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word 'recepient' is misspelled; consider renaming it to 'recipient' for clarity.

Suggested change
recepient: string;
recipient: string;

Copilot uses AI. Check for mistakes.
chain: string;
amount: string;
tokenId: string;
};

export type SwapLocalAction = {
kind: 'swap';
chain: string;
spendTokenId: string;
spendInput: string;
receiveTokenId: string;
receiveInput: string;
};

// TODO add bridge actions when merged

export type LocalAction = SendLocalAction | SwapLocalAction;
type State = {
version: 1;
localActions: Record<string, LocalAction & BaseLocalAction>;
};

export class LocalActionsStore extends PersistentStore<State> {
static getActionLink(action: LocalAction) {
if (action.kind === 'swap') {
const searchParams = new URLSearchParams({
chainInput: action.chain,
spendTokenInput: action.spendTokenId,
spendInput: action.spendInput,
receiveTokenInput: action.receiveTokenId,
});
return `/swap-form/?${searchParams.toString()}`;
}
if (action.kind === 'send') {
const searchParams = new URLSearchParams({
addressInputValue: action.recepient,
tokenValue: action.amount,
tokenAssetCode: action.tokenId,
});
return `/send-form/?${searchParams.toString()}`;
}
return null;
}

saveAction(key: string, action: LocalAction) {
this.setState((state) => ({
...state,
localActions: {
...state.localActions,
[key]: {
...action,
datetime: new Date().getTime(),
},
},
}));
}
}

export const localActionsStore = new LocalActionsStore(
{ version: 1, localActions: {} },
'localActions'
);