Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves the resource fee display on the send flow by adding a detailed fee breakdown pane to the review transaction screen, showing inclusion fee and resource fee separately for Soroban transactions.
Changes:
- Added a new "Fee breakdown" pane in
ReviewTransactionshowing inclusion fee, resource fee, and total fee with contextual descriptions for classic vs Soroban transactions - Propagated
inclusionFeeandresourceFeefrom simulation results through both send and collectible flows - Added "Calculating..." loading state for fees and a loading indicator on the continue button during simulation
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx | New fees breakdown pane with fee detail rows and info button |
| extension/src/popup/components/InternalTransaction/ReviewTransaction/styles.scss | Styles for the fees breakdown pane and info button |
| extension/src/popup/components/send/SendAmount/index.tsx | Show "Calculating..." during simulation, loading state on button |
| extension/src/popup/components/send/SendAmount/hooks/useSimulateTxData.tsx | Pass inclusionFee and resourceFee from simulation |
| extension/src/popup/components/sendCollectible/SelectedCollectible/hooks/useSimulateTxData.ts | Pass inclusionFee and resourceFee from simulation |
| extension/src/popup/locales/en/translation.json | New i18n keys for fee breakdown UI |
| extension/src/popup/locales/pt/translation.json | Portuguese translations for new keys (with one regression) |
| @shared/api/internal.ts | Added network_url to simulate-tx request body |
| extension/e2e-tests/reviewTxFees.test.ts | E2E tests for fee breakdown in token, classic, and collectible sends |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
….com:stellar/freighter into feature/improve-resource-fee-display-on-send
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@leofelix077 looks nice! few notes:
|
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
@sdfcharles applied the adjustments to the UI parts now. thanks for checking!
|
- Extract SimulateTxData interface to types/transactions.ts to remove duplicate definitions in send and collectible hooks - Both hooks now re-export the shared type - FeesPane and ReviewTransaction import directly from types/transactions - Fix FeesPane top/bottom padding and reset padding inside multi-pane-slider Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…//github.com/stellar/freighter into feature/improve-resource-fee-display-on-send
| body: JSON.stringify({ | ||
| xdr, | ||
| network_passphrase: networkDetails.networkPassphrase, | ||
| network_url: networkDetails.sorobanRpcUrl, |
There was a problem hiding this comment.
why was this change needed? It's actually only used for Soroswap, which is disabled right now
There was a problem hiding this comment.
it was throwing an error when trying to submit a collectible, mentioning it's a required field by the backend. on my MP collectibles
There was a problem hiding this comment.
Adding supporting evidence on Piyal's question, since it's still open:
Checked the current freighter-backend master — /simulate-tx in src/route/index.ts L1294-1337:
schema: {
body: {
type: "object",
required: ["xdr", "network_passphrase"], // network_url NOT required
properties: {
xdr: { type: "string" },
network_passphrase: { type: "string" }, // network_url not even listed
},
},
},
handler: async (request) => {
const { xdr, network_passphrase } = request.body; // network_url not read
// ... server derived from network_passphrase via getServer(networkName, stellarRpcConfig)
}No additionalProperties: false on the schema, so unknown fields are silently accepted and dropped — the handler never sees network_url. /simulate-token-transfer L1352-1383 also doesn't list it.
@leofelix077 — worth double-checking: the backend error you saw about "required field" might have been from a different field (e.g. a required entry you were missing), from a stale/fork backend, or a different endpoint. On current freighter-backend/master this field is dead on the wire. If the error was reproducible, a backend stack trace or the exact error payload would help pin it down before we land the client change.
Either the freighter-backend needs a companion PR to actually use network_url, or this line can be removed.
| </> | ||
| ) : null} | ||
| {isEditingSettings ? ( | ||
| {isEditingSettings && !isShowingFeesPane ? ( |
There was a problem hiding this comment.
1 small UX improvement we can make here. I noticed if you edit the fee, then click the inclusion fee i bubble, it resets the fee field. A user would lose any change they've made in this field
Screen.Recording.2026-03-25.at.12.01.33.PM.mov
There was a problem hiding this comment.
nice one!
I adjusted the fee persistence inside the flow, so if user manually edits it, it's kept inside the context (soroban vs classic) and won't change again after explicit user input, unless user exits the transaction flow
Screen.Recording.2026-04-02.at.13.47.14.mov
PR Review: Improve fees display on send (Extension)Cross-reference: This review covers this PR alongside the mobile counterpart at stellar/freighter-mobile#774. A comparison section is included at the bottom. Individual Review of Extension PROverall: Clean, well-structured implementation. The Things I Like
Issues & Suggestions1. Auto-simulation useEffect(() => {
if (simulationState.state === RequestState.LOADING) return;
// ...
if (destChanged || assetChanged) {
fetchSimulationData();
}
}, [destination, asset, isToken, isCollectible, simulationState.state]);Including 2. {simulationState.data?.resourceFee
? t("Fees description soroban")
: t("Fees description classic")}This works for the fee breakdown description, but has a subtle issue: while loading ( 3. 4. Info button always shown on ReviewTransaction fee row 📝 <button
className="ReviewTx__Details__Row__FeesInfoBtn"
data-testid="review-tx-fee-info-btn"
onClick={() => setActivePaneIndex(paneConfig.feesIndex)}
>
<Icon.InfoCircle />
</button>The info circle button is always rendered (both Soroban and classic). This is fine since the 5. body: JSON.stringify({
xdr,
network_passphrase: networkDetails.networkPassphrase,
network_url: networkDetails.sorobanRpcUrl,
}),Small but important change in 6. Fee display shows "Calculating..." only for Soroban ✅ Cross-Platform Comparison: Extension (#2649) vs Mobile (#774)Both PRs implement the same feature (Soroban fee breakdown: Inclusion Fee + Resource Fee + Total Fee) across the two Freighter platforms. Here's how they differ:
Key Architectural Differences Worth Aligning
Generated by Claude Code |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Extract SimulateTxData interface to types/transactions.ts to remove duplicate definitions in send and collectible hooks - Both hooks now re-export the shared type - FeesPane and ReviewTransaction import directly from types/transactions - Fix FeesPane top/bottom padding and reset padding inside multi-pane-slider Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…//github.com/stellar/freighter into feature/improve-resource-fee-display-on-send
|
@CassioMG @piyalbasu I adjusted the last points on the code and checked the Claude comments most of the comments are related to data source and state management, which are inherently different on mobile vs extension, and some of the UI parts that are also different on Mobile (e.g. on mobile we have the info sheet for the transaction fee, on extension we always show the fees breakdown) Funcionality-wise they should be in parity now |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .FeesPaneOverlay { | ||
| position: absolute; | ||
| top: 0; | ||
| left: 0; | ||
| width: 100%; | ||
| height: 100%; | ||
| overflow-y: auto; | ||
| z-index: var(--z-index--banner); | ||
| background: var(--sds-clr-gray-01); | ||
| padding: pxToRem(24px); | ||
| box-sizing: border-box; | ||
| } | ||
|
|
There was a problem hiding this comment.
.FeesPaneOverlay appears to be unused (no references outside this stylesheet). If it’s leftover from an earlier approach, removing it would reduce dead CSS; otherwise, wire it up where FeesPane is rendered so it’s not orphaned.
| .FeesPaneOverlay { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow-y: auto; | |
| z-index: var(--z-index--banner); | |
| background: var(--sds-clr-gray-01); | |
| padding: pxToRem(24px); | |
| box-sizing: border-box; | |
| } |
| import { BlockAidScanTxResult } from "@shared/api/types"; | ||
|
|
||
| export interface FlaggedKeys { | ||
| [address: string]: { | ||
| tags: Array<string>; | ||
| }; | ||
| } | ||
|
|
||
| export interface SimulateTxData { | ||
| transactionXdr: string; | ||
| scanResult?: BlockAidScanTxResult | null; | ||
| inclusionFee?: string; | ||
| resourceFee?: string; | ||
| } |
There was a problem hiding this comment.
This file imports BlockAidScanTxResult as a value import, but it’s only used as a type. Prefer import type { BlockAidScanTxResult ... } to ensure the import is elided in emitted JS and avoid accidentally pulling runtime code into the extension bundle.
| <button | ||
| className="ReviewTx__Details__Row__FeesInfoBtn" | ||
| data-testid="review-tx-fee-info-btn" | ||
| onClick={() => setActivePaneIndex(paneConfig.feesIndex)} | ||
| aria-label={t("Fee breakdown")} | ||
| > | ||
| <Icon.InfoCircle /> | ||
| </button> |
There was a problem hiding this comment.
The fee info icon is rendered as a <button> without an explicit type. To avoid defaulting to type="submit" if this component is ever rendered inside a form, set type="button" (consistent with the other icon buttons in this PR).
| onShowFeesInfo={(currentDraftFee) => { | ||
| const inclusionFee = | ||
| currentDraftFee || | ||
| (lastInclusionFeeRef.current ?? recommendedFee); | ||
| setFeesPaneTotal( | ||
| buildFeesPaneTotal( | ||
| inclusionFee, | ||
| simulationState.data?.resourceFee, | ||
| ), | ||
| ); | ||
| setIsShowingFeesPane(true); | ||
| }} |
There was a problem hiding this comment.
feesPaneTotal is captured only when opening the FeesPane. If the pane is opened while a Soroban simulation is still loading (so simulationState.data?.resourceFee is unavailable), the FeesPane will later render inclusion/resource rows but the Total Fee row can remain stuck at the pre-simulation value. Consider deriving the total in render (or in FeesPane) from the current draft inclusion fee plus simulationState.data.resourceFee, or updating feesPaneTotal via an effect while the pane is open and simulation data arrives.
|
|
||
| const isLoading = | ||
| simulationState.state === RequestState.IDLE || | ||
| simulationState.state === RequestState.LOADING; |
There was a problem hiding this comment.
Critical — FeesPane renders a misleading Total on RequestState.ERROR
const isLoading =
simulationState.state === RequestState.IDLE ||
simulationState.state === RequestState.LOADING;ERROR is neither IDLE nor LOADING, so !isLoading is true — but simulationState.data is null. The Inclusion Fee row (line 48) and Resource Fee row (line 62) gate on simulationState.data?.inclusionFee / .resourceFee and hide, while the Total Fee row at line 76-82 hits the ${fee} XLM branch.
Result: when Soroban simulation fails, FeesPane silently renders a "Total Fee" that is just the base inclusion fee (≈ 0.00001 XLM), with no breakdown rows and no error indication. The user thinks that's the real cost of a ~0.5 XLM Soroban tx.
This is exactly the failure mode the feature is supposed to eliminate, appearing in the opposite direction. Fix: add an explicit error branch. For example:
const isError = simulationState.state === RequestState.ERROR;
// in the Total row:
{isLoading ? t("Calculating...") : isError ? "—" : `${fee} XLM`}And render an error message in the card body so the user knows the breakdown is unavailable rather than "0.00001 XLM".
There was a problem hiding this comment.
this may also be present on mobile's PR
| ? `${fee} ${t("XLM")}` | ||
| : recommendedFeeUsd} | ||
| {(isToken || isCollectible) && | ||
| simulationState.state === RequestState.LOADING |
There was a problem hiding this comment.
Critical — send-amount-fee-display shows a misleading total on Soroban simulation failure
This block handles LOADING ("Calculating…") but not ERROR. On RequestState.ERROR, control falls through to the inputType === "crypto" ? \${fee} ${t("XLM")}` : recommendedFeeUsdbranch, wherefee = transactionFee || recommendedFee. If the user hasn't edited the inclusion fee, they see 0.00001 XLM` (the base inclusion) as the "Fee" for a Soroban tx whose real cost is much higher.
Compounding: the Review-Send button at line 615 only adds isLoading for LOADING, so on ERROR the button stays enabled — user can proceed to review and sign, still seeing the wrong fee.
Fix: either treat ERROR the same as LOADING for display (show Calculating... or an em-dash) and disable the Continue button, or surface an inline error next to the fee with a retry affordance.
There was a problem hiding this comment.
this may also be present on mobile's PR
| onClose: () => void; | ||
| } | ||
|
|
||
| export const FeesPane = ({ |
There was a problem hiding this comment.
Important — No component tests for FeesPane
FeesPane is a new 97-line component with distinct rendering branches for:
RequestState.IDLE/LOADING(spinner row +Calculating...total)RequestState.SUCCESSwith Soroban data (inclusion + resource + total)RequestState.SUCCESSclassic (only total)RequestState.ERROR(currently broken — see C1)- Soroban vs. classic description copy via
isSorobanprop
The 823-line e2e test (reviewTxFees.test.ts) is thorough for the happy path and custom-fee lifecycle, but it doesn't cover the failure states or the branching logic directly — and it exercises through the whole flow, which is expensive and slow compared to a unit test.
Please add Jest/RTL component tests for FeesPane covering at least the five branches above. These would have caught C1 immediately, and would also guard against future regressions when the pane is reused in other flows (Swap already uses it today, Soroban-swap will need the breakdown fields later).
There was a problem hiding this comment.
this may also be needed on mobile's PR







closes #2475
Screen.Recording.2026-03-13.at.11.42.36.mov
Screen.Recording.2026-03-13.at.11.43.27.mov