Skip to content

Commit 3dc2779

Browse files
authored
Merge pull request #12 from agglayer/refactor-bridge-transaction-modal
Update design of bridge transaction modal
2 parents c9083d2 + fb5c734 commit 3dc2779

7 files changed

Lines changed: 188 additions & 175 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use client';
2+
3+
import { ArrowRight } from 'lucide-react';
4+
import Image from 'next/image';
5+
6+
interface BridgeRouteProps {
7+
fromChainIcon?: string;
8+
toChainIcon?: string;
9+
fromChainName: string;
10+
toChainName: string;
11+
}
12+
13+
const ChainInline = ({ icon, name }: { icon?: string; name: string }) => (
14+
<span className="flex items-center gap-2">
15+
{icon ? (
16+
<Image alt={`${name} logo`} height={100} width={100} className="size-7 rounded-sm" src={icon} />
17+
) : (
18+
<span className="size-7 rounded-sm bg-grey" />
19+
)}
20+
<span className="text-lg font-semibold text-black">{name}</span>
21+
</span>
22+
);
23+
24+
export const BridgeRoute = ({
25+
fromChainIcon,
26+
toChainIcon,
27+
fromChainName,
28+
toChainName,
29+
}: BridgeRouteProps) => {
30+
return (
31+
<div className="grid w-full grid-cols-[1fr_auto_1fr] items-center">
32+
<div className="flex justify-end pr-2">
33+
<ChainInline icon={fromChainIcon} name={fromChainName} />
34+
</div>
35+
<ArrowRight className="size-3.5 text-grey" />
36+
<div className="flex justify-start pl-2">
37+
<ChainInline icon={toChainIcon} name={toChainName} />
38+
</div>
39+
</div>
40+
);
41+
};

app/components/bridge/bridgeTransactionModal/bridgeSummary.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@ export const BridgeSummary = ({
3333
}, [alternateHeadline, headline, shouldAlternate, useAlternate]);
3434

3535
return (
36-
<div className="space-y-2 text-center">
36+
<div className="space-y-2 text-center">
3737
<div className="text-lg font-semibold text-black flex items-center justify-center gap-2">
3838
{showLoader && <Loader2 className="size-5 text-blue animate-spin" />}
3939
<span>{displayedHeadline}</span>
4040
</div>
41-
<p className=" text-muted">{subheadline}</p>
41+
<p className="text-muted text-sm">{subheadline}</p>
4242
</div>
4343
);
4444
};

app/components/bridge/bridgeTransactionModal/bridgeTransactionModal.tsx

Lines changed: 19 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,19 @@
11
'use client';
22

33
import { CheckCircle2, Link as LinkIcon, XCircle } from 'lucide-react';
4-
import { Modal } from '@/app/components/ui/modal';
5-
import { Button } from '@/app/components/ui/button';
6-
import type { BridgeExecutionState, BridgeStep as BridgeStepType } from '@/app/types/bridge';
7-
import type { Token } from '@/app/types/token';
8-
import { BridgeStep, StepState } from '@/app/components/bridge/bridgeTransactionModal/bridgeStep';
4+
import { BridgeStep } from '@/app/components/bridge/bridgeTransactionModal/bridgeStep';
95
import { BridgeSuccessView } from '@/app/components/bridge/bridgeTransactionModal/bridgeSuccessView';
10-
import { RouteVisual } from '@/app/components/bridge/bridgeTransactionModal/routeVisual';
116
import { BridgeSummary } from '@/app/components/bridge/bridgeTransactionModal/bridgeSummary';
12-
13-
type ModalMode = 'pending' | 'success' | 'error';
14-
15-
type ModalContent = {
16-
mode: ModalMode;
17-
headline: string;
18-
subheadline: string;
19-
showLoader: boolean;
20-
shouldAlternateHeadline: boolean;
21-
approveStepState: StepState;
22-
bridgeStepState: StepState;
23-
};
24-
25-
type ModalContext = {
26-
tokenSymbol: string;
27-
amount: string;
28-
fromChainName: string;
29-
toChainName: string;
30-
needsApproval: boolean;
31-
};
7+
import { BridgeRoute } from '@/app/components/bridge/bridgeTransactionModal/bridgeRoute';
8+
import { resolveBridgeTransactionModalContent } from '@/app/components/bridge/bridgeTransactionModal/bridgeTransactionModalContent';
9+
import { Button } from '@/app/components/ui/button';
10+
import { Modal } from '@/app/components/ui/modal';
11+
import { cn } from '@/app/utils/common';
12+
import type { BridgeExecutionState } from '@/app/types/bridge';
13+
import type { Token } from '@/app/types/token';
3214

3315
const ALTERNATE_HEADLINE = 'Do not refresh page';
3416

35-
const getStepState = (
36-
step: 'approve' | 'bridge',
37-
currentStep: BridgeStepType,
38-
hasApprovalTx: boolean,
39-
hasBridgeTx: boolean,
40-
): StepState => {
41-
if (step === 'approve') {
42-
if (currentStep === 'approving') return 'pending';
43-
if (currentStep === 'error' && hasApprovalTx && !hasBridgeTx) return 'error';
44-
if (hasApprovalTx) return 'success';
45-
return 'idle';
46-
}
47-
if (currentStep === 'bridging') return 'pending';
48-
if (currentStep === 'success') return 'success';
49-
if (currentStep === 'error' && hasBridgeTx) return 'error';
50-
return 'idle';
51-
};
52-
53-
const formatErrorMessage = (message?: string): string => {
54-
if (!message) return 'Something went wrong. Please try again.';
55-
const normalized = message.toLowerCase();
56-
if (normalized.includes('user rejected') || normalized.includes('rejected the request')) {
57-
return 'You rejected the request.';
58-
}
59-
return 'Something went wrong. Please try again.';
60-
};
61-
62-
const resolveModalContent = (state: BridgeExecutionState, context: ModalContext): ModalContent => {
63-
const { tokenSymbol, amount, fromChainName, toChainName } = context;
64-
const { currentStep, isExecuting, approvalTxHash, bridgeTxHash, error } = state;
65-
66-
const hasApprovalTx = Boolean(approvalTxHash);
67-
const hasBridgeTx = Boolean(bridgeTxHash);
68-
const hasSubmittedTx = hasApprovalTx || hasBridgeTx;
69-
70-
const approveStepState = getStepState('approve', currentStep, hasApprovalTx, hasBridgeTx);
71-
const bridgeStepState = getStepState('bridge', currentStep, hasApprovalTx, hasBridgeTx);
72-
73-
if (currentStep === 'success') {
74-
return {
75-
mode: 'success',
76-
headline: 'Transaction successful',
77-
subheadline: 'Your assets are on the way.',
78-
showLoader: false,
79-
shouldAlternateHeadline: false,
80-
approveStepState,
81-
bridgeStepState,
82-
};
83-
}
84-
85-
if (currentStep === 'error') {
86-
return {
87-
mode: 'error',
88-
headline: 'Transaction failed',
89-
subheadline: formatErrorMessage(error?.message),
90-
showLoader: false,
91-
shouldAlternateHeadline: false,
92-
approveStepState,
93-
bridgeStepState,
94-
};
95-
}
96-
97-
const bridgingDescription = `Bridging ${amount} ${tokenSymbol} from ${fromChainName} to ${toChainName}.`;
98-
const isAwaitingConfirmation = isExecuting && hasSubmittedTx;
99-
100-
if (currentStep === 'approving') {
101-
return {
102-
mode: 'pending',
103-
headline: `Approve ${tokenSymbol}`,
104-
subheadline: bridgingDescription,
105-
showLoader: true,
106-
shouldAlternateHeadline: isAwaitingConfirmation,
107-
approveStepState,
108-
bridgeStepState,
109-
};
110-
}
111-
112-
if (currentStep === 'bridging') {
113-
return {
114-
mode: 'pending',
115-
headline: 'Bridging assets',
116-
subheadline: bridgingDescription,
117-
showLoader: true,
118-
shouldAlternateHeadline: isAwaitingConfirmation,
119-
approveStepState,
120-
bridgeStepState,
121-
};
122-
}
123-
124-
return {
125-
mode: 'pending',
126-
headline: 'Confirm in wallet',
127-
subheadline: bridgingDescription,
128-
showLoader: true,
129-
shouldAlternateHeadline: false,
130-
approveStepState,
131-
bridgeStepState,
132-
};
133-
};
134-
13517
interface BridgeTransactionModalProps {
13618
open: boolean;
13719
onClose: () => void;
@@ -159,18 +41,17 @@ export const BridgeTransactionModal = ({
15941
needsApproval,
16042
explorerUrl,
16143
}: BridgeTransactionModalProps) => {
162-
const content = resolveModalContent(state, {
44+
const content = resolveBridgeTransactionModalContent(state, {
16345
tokenSymbol: token.symbol,
16446
amount,
16547
fromChainName,
16648
toChainName,
167-
needsApproval,
16849
});
16950

170-
const isPending = state.isExecuting;
51+
const isExecuting = state.isExecuting;
17152

17253
const handleClose = () => {
173-
if (isPending) return;
54+
if (isExecuting) return;
17455
onClose();
17556
};
17657

@@ -179,21 +60,22 @@ export const BridgeTransactionModal = ({
17960
open={open}
18061
onClose={handleClose}
18162
title="Bridge assets"
182-
showCloseButton={!isPending}
183-
dismissible={!isPending}
63+
showCloseButton={!isExecuting}
64+
dismissible={!isExecuting}
18465
contentClassName="space-y-6"
18566
>
18667
<div className="space-y-4 text-center">
18768
{content.mode === 'success' || content.mode === 'error' ? (
18869
<div
189-
className={`mx-auto flex size-14 items-center justify-center rounded-full ${
190-
content.mode === 'success' ? 'bg-green-light text-green' : 'bg-red-light text-red'
191-
}`}
70+
className={cn(
71+
'mx-auto flex size-14 items-center justify-center rounded-full',
72+
content.mode === 'success' ? 'bg-green-light text-green' : 'bg-red-light text-red',
73+
)}
19274
>
19375
{content.mode === 'success' ? <CheckCircle2 className="size-7" /> : <XCircle className="size-7" />}
19476
</div>
19577
) : (
196-
<RouteVisual
78+
<BridgeRoute
19779
fromChainIcon={fromChainIcon}
19880
toChainIcon={toChainIcon}
19981
fromChainName={fromChainName}
@@ -234,7 +116,7 @@ export const BridgeTransactionModal = ({
234116
/>
235117
)}
236118
{content.mode === 'error' && (
237-
<div className="space-y-4 text-center">
119+
<div className="space-y-3 text-center">
238120
{state.error?.txHash && explorerUrl && (
239121
<a
240122
href={`${explorerUrl}/tx/${state.error.txHash}`}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import type { StepState } from '@/app/components/bridge/bridgeTransactionModal/bridgeStep';
2+
import type { BridgeExecutionState, BridgeStep as BridgeStepType } from '@/app/types/bridge';
3+
4+
export type BridgeTransactionModalMode = 'pending' | 'success' | 'error';
5+
6+
export type BridgeTransactionModalContent = {
7+
mode: BridgeTransactionModalMode;
8+
headline: string;
9+
subheadline: string;
10+
showLoader: boolean;
11+
shouldAlternateHeadline: boolean;
12+
approveStepState: StepState;
13+
bridgeStepState: StepState;
14+
};
15+
16+
export type BridgeTransactionModalContext = {
17+
tokenSymbol: string;
18+
amount: string;
19+
fromChainName: string;
20+
toChainName: string;
21+
};
22+
23+
const getStepState = (
24+
step: 'approve' | 'bridge',
25+
currentStep: BridgeStepType,
26+
hasApprovalTx: boolean,
27+
hasBridgeTx: boolean,
28+
): StepState => {
29+
if (step === 'approve') {
30+
if (currentStep === 'approving') return 'pending';
31+
if (currentStep === 'error' && hasApprovalTx && !hasBridgeTx) return 'error';
32+
if (hasApprovalTx) return 'success';
33+
return 'idle';
34+
}
35+
if (currentStep === 'bridging') return 'pending';
36+
if (currentStep === 'success') return 'success';
37+
if (currentStep === 'error' && hasBridgeTx) return 'error';
38+
return 'idle';
39+
};
40+
41+
const formatErrorMessage = (message?: string): string => {
42+
if (!message) return 'Something went wrong. Please try again.';
43+
const normalized = message.toLowerCase();
44+
if (normalized.includes('user rejected') || normalized.includes('rejected the request')) {
45+
return 'You rejected the request.';
46+
}
47+
return 'Something went wrong. Please try again.';
48+
};
49+
50+
export const resolveBridgeTransactionModalContent = (
51+
state: BridgeExecutionState,
52+
context: BridgeTransactionModalContext,
53+
): BridgeTransactionModalContent => {
54+
const { tokenSymbol, amount, fromChainName, toChainName } = context;
55+
const { currentStep, isExecuting, approvalTxHash, bridgeTxHash, error } = state;
56+
57+
const hasApprovalTx = Boolean(approvalTxHash);
58+
const hasBridgeTx = Boolean(bridgeTxHash);
59+
const hasSubmittedTx = hasApprovalTx || hasBridgeTx;
60+
61+
const approveStepState = getStepState('approve', currentStep, hasApprovalTx, hasBridgeTx);
62+
const bridgeStepState = getStepState('bridge', currentStep, hasApprovalTx, hasBridgeTx);
63+
64+
if (currentStep === 'success') {
65+
return {
66+
mode: 'success',
67+
headline: 'Transaction successful',
68+
subheadline: 'Your assets are on the way.',
69+
showLoader: false,
70+
shouldAlternateHeadline: false,
71+
approveStepState,
72+
bridgeStepState,
73+
};
74+
}
75+
76+
if (currentStep === 'error') {
77+
return {
78+
mode: 'error',
79+
headline: 'Transaction failed',
80+
subheadline: formatErrorMessage(error?.message),
81+
showLoader: false,
82+
shouldAlternateHeadline: false,
83+
approveStepState,
84+
bridgeStepState,
85+
};
86+
}
87+
88+
const bridgingDescription = `Bridging ${amount} ${tokenSymbol} from ${fromChainName} to ${toChainName}.`;
89+
const isAwaitingConfirmation = isExecuting && hasSubmittedTx;
90+
91+
if (currentStep === 'approving') {
92+
return {
93+
mode: 'pending',
94+
headline: `Approve ${tokenSymbol}`,
95+
subheadline: bridgingDescription,
96+
showLoader: true,
97+
shouldAlternateHeadline: isAwaitingConfirmation,
98+
approveStepState,
99+
bridgeStepState,
100+
};
101+
}
102+
103+
if (currentStep === 'bridging') {
104+
return {
105+
mode: 'pending',
106+
headline: 'Bridging assets',
107+
subheadline: bridgingDescription,
108+
showLoader: true,
109+
shouldAlternateHeadline: isAwaitingConfirmation,
110+
approveStepState,
111+
bridgeStepState,
112+
};
113+
}
114+
115+
return {
116+
mode: 'pending',
117+
headline: 'Confirm in wallet',
118+
subheadline: bridgingDescription,
119+
showLoader: true,
120+
shouldAlternateHeadline: false,
121+
approveStepState,
122+
bridgeStepState,
123+
};
124+
};

0 commit comments

Comments
 (0)