From 948c1278c0377b18e8cab37587b6607b26607571 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Thu, 18 Jun 2026 16:56:55 -0300 Subject: [PATCH 01/14] add partial flow redesign for add sac token --- extension/e2e-tests/helpers/stubs.ts | 36 ++ .../freighterApiIntegration.test.ts | 146 ++++++++ .../__tests__/ChangeTrustInternal.test.tsx | 351 ++++++++++++++++++ .../ChangeTrustInternal/index.tsx | 54 ++- .../src/popup/helpers/useChangeTrustline.ts | 142 ------- .../src/popup/helpers/useSetupAddTokenFlow.ts | 20 +- .../src/popup/locales/en/translation.json | 3 + .../src/popup/locales/pt/translation.json | 3 + .../AddToken/__tests__/AddToken.test.tsx | 320 ++++++++++++++++ extension/src/popup/views/AddToken/index.tsx | 43 ++- 10 files changed, 954 insertions(+), 164 deletions(-) create mode 100644 extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx delete mode 100644 extension/src/popup/helpers/useChangeTrustline.ts create mode 100644 extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx diff --git a/extension/e2e-tests/helpers/stubs.ts b/extension/e2e-tests/helpers/stubs.ts index 76762a49f1..28c174dc56 100644 --- a/extension/e2e-tests/helpers/stubs.ts +++ b/extension/e2e-tests/helpers/stubs.ts @@ -651,6 +651,42 @@ export const stubIsSac = async (page: Page | BrowserContext) => { }); }; +/** + * SAC variant of stubIsSac — returns isSacContract: true so the AddToken view + * routes through the ChangeTrustInternal review instead of silently resolving. + * Use together with stubSacTokenDetails so token-details returns a "CODE:G…" + * name that satisfies StrKey.isValidEd25519PublicKey(assetIssuer). + */ +export const stubIsSacTrue = async (page: Page | BrowserContext) => { + await page.route("**/is-sac-contract**", async (route) => { + const json = { + isSacContract: true, + }; + await route.fulfill({ json }); + }); +}; + +// The G-address used across the e2e suite as the SAC issuer. +export const SAC_ISSUER = + "GDF32CQINROD3E2LMCGZUDVMWTXCJFR5SBYVRJ7WAAIAS3P7DCVWZEFY"; + +/** + * Stubs token-details so the SAC token's name is "E2E:". + * useTokenLookup splits on ":" to extract the issuer, and AddToken checks + * StrKey.isValidEd25519PublicKey(assetIssuer) to gate the SAC review branch. + */ +export const stubSacTokenDetails = async (page: Page | BrowserContext) => { + await page.route("**/token-details/**", async (route) => { + await route.fulfill({ + json: { + name: `E2E:${SAC_ISSUER}`, + decimals: 7, + symbol: "E2E", + }, + }); + }); +}; + export const stubTokenDetails = async (page: Page | BrowserContext) => { await page.route("**/token-details/**", async (route) => { const url = route.request().url(); diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index 6ef3c9b46d..0049027215 100644 --- a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -3,10 +3,18 @@ import { TEST_TOKEN_ADDRESS } from "../helpers/test-token"; import { loginToTestAccount, switchToMainnet } from "../helpers/login"; import { allowDapp } from "../helpers/dAppSessionHelper"; import { + SAC_ISSUER, stubAccountBalances, stubAccountHistory, + stubBackendSubmitTx, + stubFeeStats, + stubHorizonAccounts, stubIsSac, + stubIsSacTrue, + stubSacTokenDetails, + stubScanAssetSafe, stubScanDapp, + stubScanTx, stubTokenDetails, stubTokenPrices, } from "../helpers/stubs"; @@ -846,6 +854,144 @@ test("should add token when allowed", async ({ ); }); +// The cryptographically-derived SAC contract for E2E:SAC_ISSUER on testnet. +// new Asset("E2E", SAC_ISSUER).contractId("Test SDF Network ; September 2015") +// Using this contract (not TEST_TOKEN_ADDRESS) is required so that +// isAssetSac() in useGetChangeTrustData verifies correctly and builds the XDR. +const SAC_CONTRACT_ID = + "CAMGWOMKYNKCWGHXTU6A7OYW3O6O4UFMHSMQDSIA2WSD6M6U6GSAJASN"; + +// SAC token test: skipped in integration mode — the stubs needed to classify +// SAC_CONTRACT_ID as a SAC must be injected before the popup opens, which +// requires the window.fetch addInitScript approach described below. In +// integration mode all stubs are bypassed and the real backend is used, but +// the test account does not have a live SAC on testnet. +test("should open the Change Trust review and add a SAC token when allowed", async ({ + page, + extensionId, + context, +}) => { + test.skip( + isIntegrationMode, + "SAC stub injection via addInitScript is not compatible with integration mode", + ); + + // Playwright's context.route() / page.route() does not reliably intercept + // fetch calls made by Chrome extension popup pages in headless mode because + // those pages run in an isolated extension process. We therefore override + // window.fetch via context.addInitScript(), which is guaranteed to run before + // ANY page script on every new page — including the popup opened by + // browser.windows.create(). This is the same technique used in + // stubAccountHistoryWith (page level) applied at context level. + // + // Endpoints overridden: + // is-sac-contract → { isSacContract: true } + // token-details → { name: "E2E:", symbol: "E2E", decimals: 7 } + // + // Effect on the AddToken flow: + // useTokenLookup receives isSacContract=true → sets issuer to the G-address + // → StrKey.isValidEd25519PublicKey(issuer) = true → isSac = true + // → Confirm click calls setShowTrustlineReview(true) instead of handleApprove() + // → ChangeTrustInternal renders, isAssetSac verifies SAC_CONTRACT_ID, builds XDR + await context.addInitScript( + ({ sacIssuer }: { sacIssuer: string }) => { + const origFetch = (window as Window & typeof globalThis).fetch.bind( + window, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).fetch = function (input: any, init: any) { + const url: string = + typeof input === "string" + ? input + : input instanceof URL + ? input.href + : (input?.url ?? ""); + if (url.includes("/is-sac-contract/")) { + return Promise.resolve( + new Response(JSON.stringify({ isSacContract: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + } + if (url.includes("/token-details/")) { + return Promise.resolve( + new Response( + JSON.stringify({ + name: `E2E:${sacIssuer}`, + symbol: "E2E", + decimals: 7, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + return origFetch(input, init); + }; + }, + { sacIssuer: SAC_ISSUER }, + ); + + // context.route() as belt-and-suspenders (intercepted when CDP cooperates) + await stubIsSacTrue(context); + await stubSacTokenDetails(context); + + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + await allowDapp({ page }); + + // Open a second tab pointing at the addToken playground. + const pageTwo = await page.context().newPage(); + await pageTwo.waitForLoadState(); + + const popupPromise = page.context().waitForEvent("page"); + await pageTwo.goto( + "https://play.freighter.app/#/extension/playground/addToken", + ); + // Use the cryptographically-derived SAC contract ID, not TEST_TOKEN_ADDRESS. + // AddToken passes this contractId to useTokenLookup and then to ChangeTrustInternal; + // isAssetSac() verifies it against Asset("E2E", SAC_ISSUER).contractId(passphrase). + await pageTwo.getByRole("textbox").first().fill(SAC_CONTRACT_ID); + await pageTwo + .getByRole("textbox") + .nth(1) + .fill("Test SDF Network ; September 2015"); + await pageTwo.getByText("Add Token").click(); + + const popup = await popupPromise; + + // Belt-and-suspenders page-level stubs on the popup for requests fired after + // the popup is captured (ChangeTrust data-loading phase). + await stubIsSacTrue(popup); + await stubSacTokenDetails(popup); + await stubFeeStats(popup); + await stubHorizonAccounts(popup); + await stubScanAssetSafe(popup); + await stubScanTx(popup); + await stubBackendSubmitTx(popup); + await stubAccountBalances(popup); + + // Wait for the token lookup to resolve (the Confirm button is hidden while + // isLoading) then click. For a SAC token the click calls + // setShowTrustlineReview(true) instead of handleApprove(). + await popup.getByTestId("add-token-approve").click(); + + // ── Assert the ChangeTrustInternal review is shown ────────────────────────── + await expect(popup.getByTestId("ChangeTrustInternal__Body")).toBeVisible({ + timeout: 15000, + }); + await expect(popup.getByText("Add Trustline")).toBeVisible(); + // SAC-specific disclosure rows rendered only when showSacDisclosure is true + await expect( + popup.getByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), + ).toBeVisible(); + await expect( + popup.getByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), + ).toContainText("0.5 XLM"); +}); + test("should not add token when not allowed", async ({ page, extensionId, diff --git a/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx b/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx new file mode 100644 index 0000000000..61e151174f --- /dev/null +++ b/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx @@ -0,0 +1,351 @@ +import React from "react"; +import { render, screen, waitFor, fireEvent } from "@testing-library/react"; + +import { BASE_FEE } from "stellar-sdk"; + +import { ChangeTrustInternal } from "popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal"; +import { Wrapper, mockBalances } from "popup/__testHelpers__"; +import { TESTNET_NETWORK_DETAILS } from "@shared/constants/stellar"; +import * as UseNetworkFees from "popup/helpers/useNetworkFees"; + +import * as GetManageAssetXDR from "popup/helpers/getManageAssetXDR"; +import * as ApiInternal from "@shared/api/internal"; +import * as CheckForSuspiciousAsset from "popup/helpers/checkForSuspiciousAsset"; +import * as BlockaidHelpers from "popup/helpers/blockaid"; +import * as StellarSdkServer from "@shared/api/helpers/stellarSdkServer"; + +const TEST_TX = { + signatureBase: () => "signatureBase", + toXDR: () => "toXDR", + _networkPassphrase: "Test SDF Network ; September 2015", + _tx: {}, + signatures: [], + fee: "100", + _envelopeType: { name: "envelopeTypeTx", value: 2 }, + _memo: {}, + _sequence: "2457228099452961", + _source: "GBKWMR7TJ7BBICOOXRY2SWXKCWPTOHZPI6MP4LNNE5A73VP3WADGG3CH", + _timeBounds: { minTime: "0", maxTime: "0" }, + operations: [ + { + type: "changeTrust", + line: { + code: "USDC", + issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + }, + limit: "922337203685.4775807", + }, + ], +}; + +jest.mock("stellar-sdk", () => { + const original = jest.requireActual("stellar-sdk"); + return { + Asset: original.Asset, + BASE_FEE: original.BASE_FEE, + Operation: original.Operation, + Networks: original.Networks, + Horizon: original.Horizon, + rpc: original.rpc, + StrKey: { + ...original.StrKey, + encodeEd25519PublicKey: () => + "GBKWMR7TJ7BBICOOXRY2SWXKCWPTOHZPI6MP4LNNE5A73VP3WADGG3CH", + }, + TransactionBuilder: { + fromXDR: () => TEST_TX as any, + }, + Keypair: { + fromPublicKey: () => ({ + signatureHint: () => "signatureHint", + }), + }, + xdr: { + DecoratedSignature: original.xdr.DecoratedSignature, + }, + }; +}); + +jest.mock("@ledgerhq/hw-transport-webhid", () => { + const original = jest.requireActual("@ledgerhq/hw-transport-webhid"); + return { + ...original, + create: () => Promise.resolve({ close: () => Promise.resolve() }), + list: () => Promise.resolve([{ close: () => Promise.resolve() }]), + request: () => Promise.resolve({ close: () => Promise.resolve() }), + }; +}); + +jest.mock("@ledgerhq/hw-app-str", () => { + return jest.fn().mockImplementation(() => { + return { + getPublicKey: () => + Promise.resolve({ + rawPublicKey: jest.fn(), + }), + signTransaction: () => Promise.resolve({ signature: "L1" }), + }; + }); +}); + +// Ledger account matching keys used by ManageAssetRows tests +const LEDGER_PUBLIC_KEY = + "GBKWMR7TJ7BBICOOXRY2SWXKCWPTOHZPI6MP4LNNE5A73VP3WADGG3CH"; + +const PRELOADED_STATE = { + auth: { + hasPrivateKey: true, + allAccounts: [ + { + hardwareWalletType: "", + imported: false, + name: "Account 1", + publicKey: "G1", + }, + { + hardwareWalletType: "", + imported: true, + name: "Account 2", + publicKey: "G2", + }, + { + hardwareWalletType: "Ledger", + imported: true, + name: "Ledger 1", + publicKey: LEDGER_PUBLIC_KEY, + }, + ], + publicKey: LEDGER_PUBLIC_KEY, + }, + settings: { + networkDetails: TESTNET_NETWORK_DETAILS, + }, +}; + +const ASSET = { + code: "USDC", + issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + image: null, + domain: "centre.io", + contract: "", +}; + +const renderComponent = (props: Record = {}) => + render( + + + , + ); + +describe("ChangeTrustInternal", () => { + jest + .spyOn(GetManageAssetXDR, "getManageAssetXDR") + .mockImplementation(() => + Promise.resolve( + "AAAAAgAAAABVZkfzT8IUCc68cala6hWfNx8vR5j+La0nQf3V+7AGYwAAAGQACLrWAAAAIQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAABgAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layf/////////8AAAAAAAAAAA==", + ), + ); + + jest + .spyOn(CheckForSuspiciousAsset, "checkForSuspiciousAsset") + .mockImplementation(() => + Promise.resolve({ isRevocable: false, isInvalidDomain: false }), + ); + + jest.spyOn(BlockaidHelpers, "scanAsset").mockImplementation(() => + Promise.resolve({ + result_type: "Benign", + address: "", + chain: "stellar", + attack_types: {}, + fees: {}, + } as any), + ); + + jest.spyOn(StellarSdkServer, "stellarSdkServer").mockImplementation( + () => + ({ + accounts: { + accountId: () => ({ + call: () => Promise.resolve({ balances: [] }), + }), + }, + }) as any, + ); + + jest + .spyOn(ApiInternal, "getAccountBalances") + .mockImplementation(() => Promise.resolve(mockBalances)); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("calls onSuccess (not onCancel) after a successful submit", async () => { + const onSuccess = jest.fn(); + const onCancel = jest.fn(); + + jest.spyOn(global, "fetch").mockImplementation(() => + Promise.resolve({ + ok: true, + json: async () => ({ + envelope_xdr: + "AAAAAgAAAABngBTmbmUycqG2cAMHcomSR80dRzGtKzxM6gb3yySD5AAPQkAAAYjdAAAA9gAAAAEAAAAAAAAAAAAAAABmXjffAAAAAAAAAAEAAAAAAAAABgAAAAFVU0RDAAAAACYFzNOyHT8GgwiyzcOOhwLtCctwM/RiSnrFp7JOe8xeAAAAAAAAAAAAAAAAAAAAAcskg+QAAABAA/rRMU+KKsxCX1pDBuCvYDz+eQTCsY9bzgPU4J+Xe3vOWUa8YOzWlL3N3zlxHVx9hsB7a8dpSXMSAINjjsY4Dg==", + hash: "hash", + successful: true, + }), + } as any), + ); + + jest.spyOn(ApiInternal, "getHiddenAssets").mockImplementation(() => + Promise.resolve({ + hiddenAssets: {}, + error: "", + }), + ); + + renderComponent({ onSuccess, onCancel }); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal__Body"), + ).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByText("Confirm")); + + await waitFor(() => + expect(screen.getByTestId("SubmitTransaction__Title")).toHaveTextContent( + "Submitting", + ), + ); + + await waitFor(() => + expect( + screen.getByTestId("SubmitTransaction__Title__Success"), + ).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByText("Done")); + + await waitFor(() => expect(onSuccess).toHaveBeenCalledTimes(1)); + expect(onCancel).not.toHaveBeenCalled(); + }); + + it("calls onCancel on Done when onSuccess is not provided (backward-compatible default)", async () => { + const onCancel = jest.fn(); + + jest.spyOn(global, "fetch").mockImplementation(() => + Promise.resolve({ + ok: true, + json: async () => ({ + envelope_xdr: "AAAAAgAAAA==", + hash: "hash", + successful: true, + }), + } as any), + ); + + jest.spyOn(ApiInternal, "getHiddenAssets").mockImplementation(() => + Promise.resolve({ + hiddenAssets: {}, + error: "", + }), + ); + + renderComponent({ onCancel }); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal__Body"), + ).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByText("Confirm")); + + await waitFor(() => + expect( + screen.getByTestId("SubmitTransaction__Title__Success"), + ).toBeInTheDocument(), + ); + + fireEvent.click(screen.getByText("Done")); + + await waitFor(() => expect(onCancel).toHaveBeenCalledTimes(1)); + }); + + it("shows Issuer and Account reserve rows only when showSacDisclosure is set", async () => { + const { rerender } = renderComponent({ showSacDisclosure: false }); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal__Body"), + ).toBeInTheDocument(), + ); + + expect( + screen.queryByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), + ).not.toBeInTheDocument(); + + rerender( + + + , + ); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), + ).toHaveTextContent("0.5 XLM"), + ); + + expect( + screen.getByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), + ).toBeInTheDocument(); + }); + + it("does not display the raw BASE_FEE stroops value as the XLM fee (regression for 100 XLM bug)", async () => { + // Reproduce the pre-fetch state of useNetworkFees, where recommendedFee is + // the raw BASE_FEE in stroops ("100"). The seed effect must NOT promote this + // into the XLM-denominated `fee` (which would render and charge "100 XLM"). + const spy = jest.spyOn(UseNetworkFees, "useNetworkFees").mockReturnValue({ + recommendedFee: BASE_FEE, + networkCongestion: "" as UseNetworkFees.NetworkCongestion, + fetchData: jest.fn().mockResolvedValue({ recommendedFee: BASE_FEE }), + }); + + renderComponent({ showSacDisclosure: true }); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal__Body"), + ).toBeInTheDocument(), + ); + + const feeValue = screen.getByTestId( + "ChangeTrustInternal__Metadata__Value__Fee", + ); + // baseFeeStroops = stroopToXlm(BASE_FEE) = "0.00001" + expect(feeValue).toHaveTextContent("0.00001 XLM"); + expect(feeValue).not.toHaveTextContent("100 XLM"); + + spy.mockRestore(); + }); +}); diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 4df5cf6f89..bde01c0355 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { Button, Icon, Loader, Notification } from "@stellar/design-system"; import { BASE_FEE, Transaction, TransactionBuilder } from "stellar-sdk"; -import { NetworkDetails } from "@shared/constants/stellar"; +import { BASE_RESERVE, NetworkDetails } from "@shared/constants/stellar"; import { RequestState } from "constants/request"; import { getCanonicalFromAsset, @@ -53,6 +53,8 @@ interface ChangeTrustInternalProps { publicKey: string; addTrustline: boolean; onCancel: () => void; + onSuccess?: () => void; + showSacDisclosure?: boolean; } export const ChangeTrustInternal = ({ @@ -61,6 +63,8 @@ export const ChangeTrustInternal = ({ publicKey, networkDetails, onCancel, + onSuccess, + showSacDisclosure = false, }: ChangeTrustInternalProps) => { const activeOptionsRef = useRef(null); const [activePaneIndex, setActivePaneIndex] = useState(0); @@ -105,6 +109,24 @@ export const ChangeTrustInternal = ({ }; }, [activeOptionsRef]); + useEffect(() => { + // `recommendedFee` from useNetworkFees starts as BASE_FEE (raw stroops, + // e.g. "100") and only becomes an XLM-denominated value after its internal + // feeStats fetch resolves. `fee` is XLM-denominated, so only seed it once + // the fetched XLM value has arrived (recommendedFee !== BASE_FEE). + // The `fee === baseFeeStroops` guard makes this a one-shot seed that does + // not clobber a user-adjusted fee. If the fetch fails, recommendedFee + // stays BASE_FEE and `fee` keeps the safe baseFeeStroops default. + if ( + showSacDisclosure && + recommendedFee !== BASE_FEE && + fee === baseFeeStroops + ) { + setFee(recommendedFee); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showSacDisclosure, recommendedFee]); + if ( state.state === RequestState.LOADING || state.state === RequestState.IDLE @@ -275,6 +297,34 @@ export const ChangeTrustInternal = ({ {`${fee} XLM`} + {showSacDisclosure && ( + <> +
+
+ + {t("Issuer")} +
+
+ +
+
+
+
+ + {t("Account reserve")} +
+
+ {`${BASE_RESERVE} XLM`} +
+
+ + )}
setActiveBodyContent(ActiveBodyContent.details)} - onSuccess={onCancel} + onSuccess={onSuccess ?? onCancel} /> )}
diff --git a/extension/src/popup/helpers/useChangeTrustline.ts b/extension/src/popup/helpers/useChangeTrustline.ts deleted file mode 100644 index 3056a25d74..0000000000 --- a/extension/src/popup/helpers/useChangeTrustline.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { useDispatch, useSelector } from "react-redux"; - -import { stellarSdkServer } from "@shared/api/helpers/stellarSdkServer"; - -import { emitMetric } from "helpers/metrics"; -import { getCanonicalFromAsset } from "helpers/stellar"; - -import { AppDispatch } from "popup/App"; -import { getManageAssetXDR } from "popup/helpers/getManageAssetXDR"; -import { METRIC_NAMES } from "popup/constants/metricsNames"; -import { - publicKeySelector, - hardwareWalletTypeSelector, -} from "popup/ducks/accountServices"; -import { settingsNetworkDetailsSelector } from "popup/ducks/settings"; -import { - resetSubmission, - signFreighterTransaction, - submitFreighterTransaction, - startHwSign, -} from "popup/ducks/transactionSubmission"; - -import { useNetworkFees } from "./useNetworkFees"; - -export const useChangeTrustline = ({ - assetCode, - assetIssuer, - setAssetSubmitting, - setIsSigningWithHardwareWallet, - setIsTrustlineErrorShowing, - setRowButtonShowing, -}: { - assetCode: string; - assetIssuer: string; - setAssetSubmitting?: (rowButtonShowing: string) => void; - setIsSigningWithHardwareWallet?: (value: boolean) => void; - setIsTrustlineErrorShowing?: (value: boolean) => void; - setRowButtonShowing?: (value: string) => void; -}): { - changeTrustline: ( - addTrustline: boolean, - successfulCallback?: () => Promise, - ) => Promise; -} => { - const dispatch: AppDispatch = useDispatch(); - const walletType = useSelector(hardwareWalletTypeSelector); - const networkDetails = useSelector(settingsNetworkDetailsSelector); - const publicKey = useSelector(publicKeySelector); - - const isHardwareWallet = !!walletType; - - const { fetchData: fetchFees } = useNetworkFees(); - - const canonicalAsset = getCanonicalFromAsset(assetCode, assetIssuer); - - const signAndSubmit = async ( - transactionXDR: string, - trackChangeTrustline: () => void, - successfulCallback?: () => Promise, - ) => { - const res = await dispatch( - signFreighterTransaction({ - transactionXDR, - network: networkDetails.networkPassphrase, - }), - ); - - if (signFreighterTransaction.fulfilled.match(res)) { - const submitResp = await dispatch( - submitFreighterTransaction({ - publicKey, - signedXDR: res.payload.signedTransaction, - networkDetails, - }), - ); - - if (submitFreighterTransaction.fulfilled.match(submitResp)) { - trackChangeTrustline(); - dispatch(resetSubmission()); - if (successfulCallback) { - await successfulCallback(); - } - } - - if (submitFreighterTransaction.rejected.match(submitResp)) { - setIsTrustlineErrorShowing?.(true); - } - - setAssetSubmitting?.(""); - setRowButtonShowing?.(""); - } - }; - - const changeTrustline = async ( - addTrustline: boolean, // false removes the trustline - successfulCallback?: () => Promise, - ) => { - setAssetSubmitting?.(canonicalAsset); - // Build the server here (on submit) rather than during render: the dApp - // popup mounts before network details hydrate, so constructing it at render - // time can hit an empty networkUrl (v16's Server throws on that). - const server = stellarSdkServer( - networkDetails.networkUrl, - networkDetails.networkPassphrase, - ); - const fees = await fetchFees(); - const transactionXDR: string = await getManageAssetXDR({ - publicKey, - assetCode, - assetIssuer, - addTrustline, - server, - recommendedFee: fees.recommendedFee, - networkDetails, - }); - - const trackChangeTrustline = () => { - emitMetric( - addTrustline - ? METRIC_NAMES.manageAssetAddAsset - : METRIC_NAMES.manageAssetRemoveAsset, - { code: assetCode, issuer: assetIssuer }, - ); - }; - - if (isHardwareWallet) { - await dispatch(startHwSign({ transactionXDR, shouldSubmit: true })); - setIsSigningWithHardwareWallet?.(true); - trackChangeTrustline(); - } else { - await signAndSubmit( - transactionXDR, - trackChangeTrustline, - successfulCallback, - ); - } - }; - - return { - changeTrustline, - }; -}; diff --git a/extension/src/popup/helpers/useSetupAddTokenFlow.ts b/extension/src/popup/helpers/useSetupAddTokenFlow.ts index f1055cd07d..ced12644bf 100644 --- a/extension/src/popup/helpers/useSetupAddTokenFlow.ts +++ b/extension/src/popup/helpers/useSetupAddTokenFlow.ts @@ -1,6 +1,5 @@ import { useState } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { StrKey } from "stellar-sdk"; import { emitMetric } from "helpers/metrics"; @@ -12,8 +11,6 @@ import { hasPrivateKeySelector, } from "popup/ducks/accountServices"; -import { useChangeTrustline } from "./useChangeTrustline"; - type Params = { rejectToken: typeof rejectToken; addToken: typeof addToken; @@ -34,8 +31,8 @@ type Response = { export const useSetupAddTokenFlow = ({ rejectToken: rejectTokenFn, addToken: addTokenFn, - assetCode, - assetIssuer, + assetCode: _assetCode, + assetIssuer: _assetIssuer, uuid, }: Params): Response => { const [isConfirming, setIsConfirming] = useState(false); @@ -44,8 +41,6 @@ export const useSetupAddTokenFlow = ({ const dispatch: AppDispatch = useDispatch(); const hasPrivateKey = useSelector(hasPrivateKeySelector); - const { changeTrustline } = useChangeTrustline({ assetCode, assetIssuer }); - const rejectAndClose = () => { emitMetric(METRIC_NAMES.tokenRejectApi); dispatch(rejectTokenFn({ uuid })); @@ -53,22 +48,13 @@ export const useSetupAddTokenFlow = ({ }; const addTokenAndClose = async () => { - const addTokenDispatch = async () => { - await dispatch(addTokenFn({ uuid })); - }; - try { - if (StrKey.isValidEd25519PublicKey(assetIssuer)) { - await changeTrustline(true, addTokenDispatch); - } else { - await addTokenDispatch(); - } + await dispatch(addTokenFn({ uuid })); await emitMetric(METRIC_NAMES.tokenAddedApi); } catch (e) { console.error(e); await emitMetric(METRIC_NAMES.tokenFailedApi); } - window.close(); }; diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 52339ec4bb..3d3ac2f79f 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -12,6 +12,7 @@ "Account migration": "Account migration", "Account Migration": "Account Migration", "Account minimum balance is too low": "Account minimum balance is too low", + "Account reserve": "Account reserve", "Active": "Active", "Add": "Add", "Add a token": "Add a token", @@ -77,6 +78,8 @@ "Authorizations": "Authorizations", "Authorize": "Authorize", "Authorized address": "Authorized address", + "Auto-lock timer": "Auto-lock timer", + "Auto-Lock Timer": "Auto-Lock Timer", "available": "available", "Back": "Back", "Balance": "Balance", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index db2c569c1c..a963a0ada6 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -12,6 +12,7 @@ "Account migration": "Migração de conta", "Account Migration": "Migração de Conta", "Account minimum balance is too low": "O saldo mínimo da conta está muito baixo", + "Account reserve": "Reserva da conta", "Active": "Ativo", "Add": "Adicionar", "Add a token": "Adicionar um token", @@ -77,6 +78,8 @@ "Authorizations": "Autorizações", "Authorize": "Autorizar", "Authorized address": "Endereço autorizado", + "Auto-lock timer": "Auto-lock timer", + "Auto-Lock Timer": "Auto-Lock Timer", "available": "disponível", "Back": "Voltar", "Balance": "Saldo", diff --git a/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx b/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx new file mode 100644 index 0000000000..59bfa5705c --- /dev/null +++ b/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx @@ -0,0 +1,320 @@ +/** + * AddToken view – SAC vs SEP-41 routing test. + * + * Key challenge: the view's `useEffect` lists `handleTokenLookup` in its deps + * array, so returning a fresh function each render triggers an infinite loop. + * We solve this by returning a `useCallback`-stabilised reference from our + * mock of `useTokenLookup`, using `jest.requireActual("react")` inside the + * factory (the only way to call hooks inside a jest.mock factory). + * + * The `parsedSearchParam` URL helper expects base64-encoded JSON, so we spy + * on it and return a plain params object to avoid the atob() crash. + */ +import React from "react"; +import { + render, + screen, + fireEvent, + waitFor, + act, +} from "@testing-library/react"; +import { AddToken } from "popup/views/AddToken"; +import { Wrapper, TEST_PUBLIC_KEY } from "popup/__testHelpers__"; +import { TESTNET_NETWORK_DETAILS } from "@shared/constants/stellar"; +import { APPLICATION_STATE } from "@shared/constants/applicationState"; +import * as UrlHelpers from "helpers/urls"; +import { addToken } from "popup/ducks/access"; +import { emitMetric } from "helpers/metrics"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; + +const SAC_ISSUER = "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"; +const SEP41_CONTRACT = + "CAAV3AE3VKD2P4TY7LWTQMMJHIJ4WOCZ5ANCIJPC3NRSERKVXNHBU2W7"; + +// --------------------------------------------------------------------------- +// Shared mutable state for useTokenLookup mock. +// Variable MUST start with "mock" (case-insensitive) for jest to allow it +// to be referenced inside jest.mock() factories (jest's hoisting scope rule). +// --------------------------------------------------------------------------- +// We use an object so the mock factory closes over the reference (not the +// value), allowing per-test updates to take effect. +const mockTokenLookupConfig = { + issuer: SAC_ISSUER, +}; + +// --------------------------------------------------------------------------- +// Module-level mocks +// --------------------------------------------------------------------------- + +jest.mock("helpers/hooks/useGetAppData", () => ({ + AppDataType: { RESOLVED: "resolved", REROUTE: "re-route" }, + useGetAppData: () => ({ + state: { + state: "SUCCESS", + data: { + type: "resolved", + account: { + publicKey: "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF", + applicationState: "MNEMONIC_PHRASE_CONFIRMED", + hasPrivateKey: true, + allAccounts: [ + { + hardwareWalletType: "", + imported: false, + name: "Account 1", + publicKey: + "GBTYAFHGNZSTE4VBWZYAGB3SRGJEPTI5I4Y22KZ4JTVAN56LESB6JZOF", + }, + ], + bipPath: "m/44'/148'/0'", + tokenIdList: [], + }, + settings: { + networkDetails: { + isTestnet: true, + network: "TESTNET", + networkName: "Test SDF Network", + otherNetworkName: "Mainnet", + networkUrl: "https://horizon-testnet.stellar.org", + networkPassphrase: "Test SDF Network ; September 2015", + }, + networksList: [], + hiddenAssets: {}, + allowList: [], + error: "", + isDataSharingAllowed: false, + isMemoValidationEnabled: false, + isHideDustEnabled: false, + isOpenSidebarByDefault: false, + assetsLists: [], + autoLockTimeoutMinutes: 30, + isExperimentalModeEnabled: false, + isHashSigningEnabled: false, + isNonSSLEnabled: false, + }, + }, + error: null, + }, + fetchData: jest.fn().mockResolvedValue(undefined), + }), +})); + +// Sentinel mock — lets us assert whether the review was shown +jest.mock( + "popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal", + () => ({ + ChangeTrustInternal: (props: any) => ( +
+ +
+ ), + }), +); + +// useTokenLookup mock that returns a *stable* handleTokenLookup via useCallback +// so the view's useEffect([contractId, handleTokenLookup]) doesn't loop. +jest.mock("popup/helpers/useTokenLookup", () => { + const { useCallback } = jest.requireActual("react"); + return { + useTokenLookup: ({ + setAssetRows, + setIsSearching, + setIsVerifiedToken, + setIsVerificationInfoShowing, + }: any) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const handleTokenLookup = useCallback(async (_contractId: string) => { + setAssetRows([ + { + code: "USDC", + // Read issuer from the shared config object (mutated per-test) + issuer: mockTokenLookupConfig.issuer, + contract: + "CAAV3AE3VKD2P4TY7LWTQMMJHIJ4WOCZ5ANCIJPC3NRSERKVXNHBU2W7", + domain: "centre.io", + }, + ]); + setIsVerifiedToken(true); + setIsVerificationInfoShowing(false); + setIsSearching(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return { handleTokenLookup }; + }, + }; +}); + +jest.mock("@shared/api/helpers/getIconUrlFromIssuer", () => ({ + getIconUrlFromIssuer: jest.fn().mockResolvedValue(""), +})); + +jest.mock("stellar-sdk", () => { + const original = jest.requireActual("stellar-sdk"); + return { + ...original, + StellarToml: { + Resolver: { + resolve: jest.fn().mockResolvedValue({ CURRENCIES: [] }), + }, + }, + }; +}); + +jest.mock("popup/helpers/blockaid", () => ({ + scanAsset: jest.fn().mockResolvedValue(null), + isAssetSuspicious: jest.fn().mockReturnValue(false), + isAssetMalicious: jest.fn().mockReturnValue(false), + shouldTreatAssetAsUnableToScan: jest.fn().mockReturnValue(false), + useIsAssetSuspicious: jest.fn().mockReturnValue(() => false), + useBlockaidOverrideState: jest.fn().mockReturnValue(null), +})); + +jest.mock("@shared/api/internal", () => ({ + getBlockaidOverrideState: jest.fn().mockResolvedValue(null), +})); + +jest.mock("popup/helpers/useIsDomainListedAllowed", () => ({ + useIsDomainListedAllowed: jest + .fn() + .mockReturnValue({ isDomainListedAllowed: true }), +})); + +jest.mock("popup/helpers/useMarkQueueActive", () => ({ + useMarkQueueActive: jest.fn(), +})); + +jest.mock("popup/helpers/route", () => ({ + reRouteOnboarding: jest.fn(), +})); + +jest.mock("helpers/metrics", () => ({ + emitMetric: jest.fn().mockResolvedValue(undefined), + storeAccountMetricsData: jest.fn(), + registerHandler: jest.fn(), +})); + +jest.mock("popup/ducks/access", () => ({ + rejectToken: jest.fn(() => ({ type: "access/rejectToken" })), + addToken: jest.fn(() => ({ type: "access/addToken" })), +})); + +jest.mock("popup/helpers/useSetupAddTokenFlow", () => ({ + useSetupAddTokenFlow: () => ({ + isConfirming: false, + isPasswordRequired: false, + setIsPasswordRequired: jest.fn(), + verifyPasswordThenAddToken: jest.fn(), + handleApprove: jest.fn(), + rejectAndClose: jest.fn(), + }), +})); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const mockState = { + auth: { + hasPrivateKey: true, + publicKey: TEST_PUBLIC_KEY, + applicationState: APPLICATION_STATE.MNEMONIC_PHRASE_CONFIRMED, + allAccounts: [ + { + hardwareWalletType: "", + imported: false, + name: "Account 1", + publicKey: TEST_PUBLIC_KEY, + }, + ], + }, + settings: { + networkDetails: TESTNET_NETWORK_DETAILS, + }, +}; + +const renderAt = () => + render( + + + , + ); + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe("AddToken SAC / SEP-41 routing", () => { + beforeEach(() => { + jest.clearAllMocks(); + // Bypass base64 URL parsing — inject params directly so atob() doesn't crash + jest.spyOn(UrlHelpers, "parsedSearchParam").mockReturnValue({ + contractId: SEP41_CONTRACT, + domain: "example.com", + url: "https://example.com", + uuid: "test-uuid", + networkPassphrase: "", + } as any); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("SAC: Confirm opens the Change Trust review instead of submitting", async () => { + // G... issuer → StrKey.isValidEd25519PublicKey = true → SAC branch + mockTokenLookupConfig.issuer = SAC_ISSUER; + renderAt(); + + const confirm = await screen.findByTestId("add-token-approve"); + fireEvent.click(confirm); + + await waitFor(() => + expect( + screen.getByTestId("ChangeTrustInternal-mock"), + ).toBeInTheDocument(), + ); + }); + + it("SAC: a successful review resolves the dApp request (addToken + metric + close)", async () => { + mockTokenLookupConfig.issuer = SAC_ISSUER; + const closeSpy = jest + .spyOn(window, "close") + .mockImplementation(() => undefined); + + renderAt(); + + const confirm = await screen.findByTestId("add-token-approve"); + fireEvent.click(confirm); + + const successBtn = await screen.findByText("mock-success"); + await act(async () => { + fireEvent.click(successBtn); + }); + + await waitFor(() => + expect(jest.mocked(addToken)).toHaveBeenCalledWith({ uuid: "test-uuid" }), + ); + expect(jest.mocked(emitMetric)).toHaveBeenCalledWith( + METRIC_NAMES.tokenAddedApi, + ); + expect(closeSpy).toHaveBeenCalled(); + }); + + it("SEP-41: Confirm does not open the Change Trust review", async () => { + // C... issuer → StrKey.isValidEd25519PublicKey = false → SEP-41 branch + mockTokenLookupConfig.issuer = SEP41_CONTRACT; + renderAt(); + + const confirm = await screen.findByTestId("add-token-approve"); + fireEvent.click(confirm); + + // Allow any pending state updates to settle + await act(async () => {}); + + expect( + screen.queryByTestId("ChangeTrustInternal-mock"), + ).not.toBeInTheDocument(); + }); +}); diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index 0ad6e50cfb..5e7362f8b6 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -10,8 +10,13 @@ import { import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { StellarToml } from "stellar-sdk"; +import { useSelector, useDispatch } from "react-redux"; +import { StellarToml, StrKey } from "stellar-sdk"; + +import { AppDispatch } from "popup/App"; +import { METRIC_NAMES } from "popup/constants/metricsNames"; +import { emitMetric } from "helpers/metrics"; +import { ChangeTrustInternal } from "popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal"; import { BlockAidScanAssetResult } from "@shared/api/types"; import { getIconUrlFromIssuer } from "@shared/api/helpers/getIconUrlFromIssuer"; @@ -101,6 +106,16 @@ export const AddToken = () => { const isLoading = isSearching || assetIcon === undefined || assetTomlName === undefined; + const dispatch: AppDispatch = useDispatch(); + const [showTrustlineReview, setShowTrustlineReview] = useState(false); + const isSac = StrKey.isValidEd25519PublicKey(assetIssuer); + + const handleSacSuccess = async () => { + await dispatch(addToken({ uuid })); + await emitMetric(METRIC_NAMES.tokenAddedApi); + window.close(); + }; + const { isConfirming, isPasswordRequired, @@ -344,6 +359,26 @@ export const AddToken = () => { ); } + if (showTrustlineReview && isSac && assetCurrency) { + return ( + setShowTrustlineReview(false)} + onSuccess={handleSacSuccess} + showSacDisclosure + /> + ); + } + return ( @@ -475,7 +510,9 @@ export const AddToken = () => { size="lg" variant={isMaliciousAsset ? "error" : "secondary"} isLoading={isConfirming} - onClick={() => handleApprove()} + onClick={() => + isSac ? setShowTrustlineReview(true) : handleApprove() + } > {t("Confirm")} From 9b8666df89b000489080d0a24efc1543c5c223d6 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Tue, 23 Jun 2026 16:37:39 -0300 Subject: [PATCH 02/14] fix flow logic to external add token --- .../freighterApiIntegration.test.ts | 20 +++---- .../__tests__/ChangeTrustInternal.test.tsx | 58 ++++--------------- .../ChangeTrustInternal/index.tsx | 57 +++--------------- .../src/popup/locales/en/translation.json | 2 +- .../src/popup/locales/pt/translation.json | 2 +- .../AddToken/__tests__/AddToken.test.tsx | 36 ++++++++++++ extension/src/popup/views/AddToken/index.tsx | 43 +++++++++++++- 7 files changed, 106 insertions(+), 112 deletions(-) diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index 0049027215..13dd3eaaee 100644 --- a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -974,22 +974,22 @@ test("should open the Change Trust review and add a SAC token when allowed", asy await stubAccountBalances(popup); // Wait for the token lookup to resolve (the Confirm button is hidden while - // isLoading) then click. For a SAC token the click calls - // setShowTrustlineReview(true) instead of handleApprove(). + // isLoading). For a SAC token the Add Token screen shows the Fee and Token + // address rows (per the Figma design) before the trustline review. + await expect(popup.getByTestId("AddToken__Metadata__Row__Fee")).toBeVisible({ + timeout: 15000, + }); + await expect( + popup.getByTestId("AddToken__Metadata__Row__TokenAddress"), + ).toBeVisible(); + + // Clicking confirm for a SAC slides in the standard changeTrust review. await popup.getByTestId("add-token-approve").click(); - // ── Assert the ChangeTrustInternal review is shown ────────────────────────── await expect(popup.getByTestId("ChangeTrustInternal__Body")).toBeVisible({ timeout: 15000, }); await expect(popup.getByText("Add Trustline")).toBeVisible(); - // SAC-specific disclosure rows rendered only when showSacDisclosure is true - await expect( - popup.getByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), - ).toBeVisible(); - await expect( - popup.getByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), - ).toContainText("0.5 XLM"); }); test("should not add token when not allowed", async ({ diff --git a/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx b/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx index 61e151174f..6f25af7b68 100644 --- a/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx +++ b/extension/src/popup/components/__tests__/ChangeTrustInternal.test.tsx @@ -1,12 +1,9 @@ import React from "react"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; -import { BASE_FEE } from "stellar-sdk"; - import { ChangeTrustInternal } from "popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal"; import { Wrapper, mockBalances } from "popup/__testHelpers__"; import { TESTNET_NETWORK_DETAILS } from "@shared/constants/stellar"; -import * as UseNetworkFees from "popup/helpers/useNetworkFees"; import * as GetManageAssetXDR from "popup/helpers/getManageAssetXDR"; import * as ApiInternal from "@shared/api/internal"; @@ -281,8 +278,8 @@ describe("ChangeTrustInternal", () => { await waitFor(() => expect(onCancel).toHaveBeenCalledTimes(1)); }); - it("shows Issuer and Account reserve rows only when showSacDisclosure is set", async () => { - const { rerender } = renderComponent({ showSacDisclosure: false }); + it("defaults the fee to the base fee in XLM when no initialFee is given", async () => { + renderComponent(); await waitFor(() => expect( @@ -290,48 +287,15 @@ describe("ChangeTrustInternal", () => { ).toBeInTheDocument(), ); - expect( - screen.queryByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), - ).not.toBeInTheDocument(); - - rerender( - - - , - ); - - await waitFor(() => - expect( - screen.getByTestId("ChangeTrustInternal__Metadata__Row__Reserve"), - ).toHaveTextContent("0.5 XLM"), + const feeValue = screen.getByTestId( + "ChangeTrustInternal__Metadata__Value__Fee", ); - - expect( - screen.getByTestId("ChangeTrustInternal__Metadata__Row__Issuer"), - ).toBeInTheDocument(); + // baseFeeStroops = stroopToXlm(BASE_FEE) = "0.00001" + expect(feeValue).toHaveTextContent("0.00001 XLM"); }); - it("does not display the raw BASE_FEE stroops value as the XLM fee (regression for 100 XLM bug)", async () => { - // Reproduce the pre-fetch state of useNetworkFees, where recommendedFee is - // the raw BASE_FEE in stroops ("100"). The seed effect must NOT promote this - // into the XLM-denominated `fee` (which would render and charge "100 XLM"). - const spy = jest.spyOn(UseNetworkFees, "useNetworkFees").mockReturnValue({ - recommendedFee: BASE_FEE, - networkCongestion: "" as UseNetworkFees.NetworkCongestion, - fetchData: jest.fn().mockResolvedValue({ recommendedFee: BASE_FEE }), - }); - - renderComponent({ showSacDisclosure: true }); + it("displays the provided initialFee (the disclosed fee == the charged fee)", async () => { + renderComponent({ initialFee: "0.0011234" }); await waitFor(() => expect( @@ -342,10 +306,8 @@ describe("ChangeTrustInternal", () => { const feeValue = screen.getByTestId( "ChangeTrustInternal__Metadata__Value__Fee", ); - // baseFeeStroops = stroopToXlm(BASE_FEE) = "0.00001" - expect(feeValue).toHaveTextContent("0.00001 XLM"); + expect(feeValue).toHaveTextContent("0.0011234 XLM"); + // Guards the old unit bug: a stroops-denominated value rendered as XLM. expect(feeValue).not.toHaveTextContent("100 XLM"); - - spy.mockRestore(); }); }); diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index bde01c0355..92e1abbc11 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { Button, Icon, Loader, Notification } from "@stellar/design-system"; import { BASE_FEE, Transaction, TransactionBuilder } from "stellar-sdk"; -import { BASE_RESERVE, NetworkDetails } from "@shared/constants/stellar"; +import { NetworkDetails } from "@shared/constants/stellar"; import { RequestState } from "constants/request"; import { getCanonicalFromAsset, @@ -54,7 +54,10 @@ interface ChangeTrustInternalProps { addTrustline: boolean; onCancel: () => void; onSuccess?: () => void; - showSacDisclosure?: boolean; + // Initial fee (in XLM) for the transaction. The external Add Token (SAC) + // flow passes the network-recommended fee it already displayed so the + // charged fee matches the disclosed one. Defaults to the base fee. + initialFee?: string; } export const ChangeTrustInternal = ({ @@ -64,7 +67,7 @@ export const ChangeTrustInternal = ({ networkDetails, onCancel, onSuccess, - showSacDisclosure = false, + initialFee, }: ChangeTrustInternalProps) => { const activeOptionsRef = useRef(null); const [activePaneIndex, setActivePaneIndex] = useState(0); @@ -78,7 +81,7 @@ export const ChangeTrustInternal = ({ const { recommendedFee } = useNetworkFees(); const baseFeeStroops = stroopToXlm(BASE_FEE).toString(); - const [fee, setFee] = useState(baseFeeStroops); + const [fee, setFee] = useState(initialFee ?? baseFeeStroops); const [timeout, setTimeout] = useState("180"); const [memo, setMemo] = useState(""); const [isSettingsSelectorOpen, setSettingsSelectorOpen] = @@ -109,24 +112,6 @@ export const ChangeTrustInternal = ({ }; }, [activeOptionsRef]); - useEffect(() => { - // `recommendedFee` from useNetworkFees starts as BASE_FEE (raw stroops, - // e.g. "100") and only becomes an XLM-denominated value after its internal - // feeStats fetch resolves. `fee` is XLM-denominated, so only seed it once - // the fetched XLM value has arrived (recommendedFee !== BASE_FEE). - // The `fee === baseFeeStroops` guard makes this a one-shot seed that does - // not clobber a user-adjusted fee. If the fetch fails, recommendedFee - // stays BASE_FEE and `fee` keeps the safe baseFeeStroops default. - if ( - showSacDisclosure && - recommendedFee !== BASE_FEE && - fee === baseFeeStroops - ) { - setFee(recommendedFee); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showSacDisclosure, recommendedFee]); - if ( state.state === RequestState.LOADING || state.state === RequestState.IDLE @@ -297,34 +282,6 @@ export const ChangeTrustInternal = ({ {`${fee} XLM`} - {showSacDisclosure && ( - <> -
-
- - {t("Issuer")} -
-
- -
-
-
-
- - {t("Account reserve")} -
-
- {`${BASE_RESERVE} XLM`} -
-
- - )}
({ useMarkQueueActive: jest.fn(), })); +jest.mock("popup/helpers/useNetworkFees", () => ({ + useNetworkFees: () => ({ + recommendedFee: "0.0011234", + networkCongestion: "Low", + fetchData: jest.fn().mockResolvedValue({ recommendedFee: "0.0011234" }), + }), +})); + jest.mock("popup/helpers/route", () => ({ reRouteOnboarding: jest.fn(), })); @@ -277,6 +285,34 @@ describe("AddToken SAC / SEP-41 routing", () => { ); }); + it("SAC: Add Token screen shows Fee and Token address rows", async () => { + mockTokenLookupConfig.issuer = SAC_ISSUER; + renderAt(); + + await screen.findByTestId("add-token-approve"); + + expect( + screen.getByTestId("AddToken__Metadata__Row__Fee"), + ).toHaveTextContent("0.0011234 XLM"); + expect( + screen.getByTestId("AddToken__Metadata__Row__TokenAddress"), + ).toBeInTheDocument(); + }); + + it("SEP-41: Add Token screen does not show Fee or Token address rows", async () => { + mockTokenLookupConfig.issuer = SEP41_CONTRACT; + renderAt(); + + await screen.findByTestId("add-token-approve"); + + expect( + screen.queryByTestId("AddToken__Metadata__Row__Fee"), + ).not.toBeInTheDocument(); + expect( + screen.queryByTestId("AddToken__Metadata__Row__TokenAddress"), + ).not.toBeInTheDocument(); + }); + it("SAC: a successful review resolves the dApp request (addToken + metric + close)", async () => { mockTokenLookupConfig.issuer = SAC_ISSUER; const closeSpy = jest diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index 5e7362f8b6..01eb9e65e0 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -11,11 +11,13 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; -import { StellarToml, StrKey } from "stellar-sdk"; +import { BASE_FEE, StellarToml, StrKey } from "stellar-sdk"; import { AppDispatch } from "popup/App"; import { METRIC_NAMES } from "popup/constants/metricsNames"; import { emitMetric } from "helpers/metrics"; +import { stroopToXlm, truncateString } from "helpers/stellar"; +import { useNetworkFees } from "popup/helpers/useNetworkFees"; import { ChangeTrustInternal } from "popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal"; import { BlockAidScanAssetResult } from "@shared/api/types"; @@ -110,6 +112,15 @@ export const AddToken = () => { const [showTrustlineReview, setShowTrustlineReview] = useState(false); const isSac = StrKey.isValidEd25519PublicKey(assetIssuer); + // `recommendedFee` is the network-recommended fee in XLM after useNetworkFees + // resolves; pre-fetch it is the raw BASE_FEE (stroops), so guard against that + // and fall back to the base fee converted to XLM. This same value is both + // displayed here and passed into the trustline review so the disclosed fee + // matches the charged fee. + const { recommendedFee } = useNetworkFees(); + const baseFeeXlm = stroopToXlm(BASE_FEE).toString(); + const displayFee = recommendedFee === BASE_FEE ? baseFeeXlm : recommendedFee; + const handleSacSuccess = async () => { await dispatch(addToken({ uuid })); await emitMetric(METRIC_NAMES.tokenAddedApi); @@ -374,7 +385,7 @@ export const AddToken = () => { networkDetails={state.data.settings.networkDetails} onCancel={() => setShowTrustlineReview(false)} onSuccess={handleSacSuccess} - showSacDisclosure + initialFee={displayFee} /> ); } @@ -476,6 +487,34 @@ export const AddToken = () => {
+ {isSac && ( + <> +
+
+ + {t("Fee")} +
+
+ {`${displayFee} XLM`} +
+
+
+
+ + {t("Token address")} +
+
+ {truncateString(contractId)} +
+
+ + )} , blockaidData ? ( From aea39a3184760ce053aa97f3967d8b3c4ce73079 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 24 Jun 2026 12:36:39 -0300 Subject: [PATCH 03/14] adjust paddings for add and remove trustline popup --- .../components/WarningMessages/index.tsx | 4 +- .../components/WarningMessages/styles.scss | 20 ++++++++ .../ChangeTrustInternal/index.tsx | 21 ++++++-- .../ChangeTrustInternal/styles.scss | 51 +++++++++++++++++++ extension/src/popup/views/AddToken/index.tsx | 1 + 5 files changed, 91 insertions(+), 6 deletions(-) diff --git a/extension/src/popup/components/WarningMessages/index.tsx b/extension/src/popup/components/WarningMessages/index.tsx index 61f0f2fda8..6ba27a4797 100644 --- a/extension/src/popup/components/WarningMessages/index.tsx +++ b/extension/src/popup/components/WarningMessages/index.tsx @@ -207,10 +207,10 @@ export const AssetListWarning = ({ const { t } = useTranslation(); const title = isVerified ? t("On your lists") : t("Not on your lists"); return ( -
+
- +

{title}

diff --git a/extension/src/popup/components/WarningMessages/styles.scss b/extension/src/popup/components/WarningMessages/styles.scss index 9617e8c054..316ed24f10 100644 --- a/extension/src/popup/components/WarningMessages/styles.scss +++ b/extension/src/popup/components/WarningMessages/styles.scss @@ -603,6 +603,26 @@ } } +// "Not on your lists" banner — brand lilac, distinct from the amber ScanMiss. +// Sizing/spacing inherit from .ScanLabel to stay consistent with the other +// banners (the mobile Figma's smaller type scale doesn't apply to extension). +.ScanAssetList { + color: var(--sds-clr-lilac-11); + background-color: var(--sds-clr-lilac-03); + + .Icon .WarningMessage__icon { + color: var(--sds-clr-lilac-09); + } + + .Message { + font-weight: var(--font-weight-medium); + } + + .ScanLabel__Action svg { + color: var(--sds-clr-lilac-11); + } +} + .BlockaidWarningModal { height: 100vh; width: 100vw; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 92e1abbc11..86d5ff537f 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -58,6 +58,10 @@ interface ChangeTrustInternalProps { // flow passes the network-recommended fee it already displayed so the // charged fee matches the disclosed one. Defaults to the base fee. initialFee?: string; + // When rendered as a full popup view (the external Add Token flow) rather + // than inside the content-sized in-app modal, fill the available height so + // the action buttons / submit footer pin to the bottom instead of floating. + isFullHeight?: boolean; } export const ChangeTrustInternal = ({ @@ -68,7 +72,11 @@ export const ChangeTrustInternal = ({ onCancel, onSuccess, initialFee, + isFullHeight = false, }: ChangeTrustInternalProps) => { + const rootClassName = `ChangeTrustInternal${ + isFullHeight ? " ChangeTrustInternal--standalone" : "" + }`; const activeOptionsRef = useRef(null); const [activePaneIndex, setActivePaneIndex] = useState(0); const [activeBodyContent, setActiveBodyContent] = useState( @@ -117,7 +125,7 @@ export const ChangeTrustInternal = ({ state.state === RequestState.IDLE ) { return ( -
+
@@ -127,7 +135,7 @@ export const ChangeTrustInternal = ({ if (state.state === RequestState.ERROR) { return ( -
+
); + // The submit/success screen (SubmitTransaction) renders its own padded + // View.Content, so make this outer one transparent in that state to avoid + // double padding (which left the footer buttons misaligned vs the content). + const isSubmitting = activeBodyContent === ActiveBodyContent.submitTx; + return ( - -
+ +
{activeBodyContent === ActiveBodyContent.details && ( <> diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss index 456d8c1f57..871ed13507 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss @@ -10,6 +10,57 @@ background-color: var(--sds-clr-gray-01); border-radius: 0.875rem; + // Full popup view (external Add Token), vs the content-sized in-app modal: + // fill the height so actions/footer pin to the bottom instead of floating. + &--standalone { + flex: 1; + min-height: 100%; + + // Grow the slider so the action row is pushed to the bottom. + > .multi-pane-slider { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + + .multi-pane-slider__container { + flex: 1; + min-height: 0; + height: auto; + } + } + + .multi-pane-slider__pane--active { + height: 100%; + min-height: 0; + } + + // Single scroll container, so transaction details scroll fully instead of + // being cut off (drops the modal's fixed max-height). + .ChangeTrustInternal__Body { + flex: 1; + min-height: 0; + overflow-y: auto; + } + + .ChangeTrustInternal__Body__Wrapper { + max-height: none; + min-height: 0; + overflow-y: visible; + } + + .ChangeTrustInternal__TransactionDetails { + overflow-y: visible; + } + + // Pin the nested submit/success footer to the bottom. + > .View__content { + flex: 1; + min-height: 0; + } + } + &__Loading { width: 100%; min-height: 30vh; diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index 01eb9e65e0..11e904bd2d 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -386,6 +386,7 @@ export const AddToken = () => { onCancel={() => setShowTrustlineReview(false)} onSuccess={handleSacSuccess} initialFee={displayFee} + isFullHeight /> ); } From a7ca9a7abc81222cb10de7ce725e17d07d5aee6e Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 24 Jun 2026 12:44:02 -0300 Subject: [PATCH 04/14] cleanup add token functions --- extension/src/popup/helpers/useSetupAddTokenFlow.ts | 4 ---- extension/src/popup/views/AddToken/index.tsx | 2 -- 2 files changed, 6 deletions(-) diff --git a/extension/src/popup/helpers/useSetupAddTokenFlow.ts b/extension/src/popup/helpers/useSetupAddTokenFlow.ts index ced12644bf..34330f6793 100644 --- a/extension/src/popup/helpers/useSetupAddTokenFlow.ts +++ b/extension/src/popup/helpers/useSetupAddTokenFlow.ts @@ -14,8 +14,6 @@ import { type Params = { rejectToken: typeof rejectToken; addToken: typeof addToken; - assetCode: string; - assetIssuer: string; uuid: string; }; @@ -31,8 +29,6 @@ type Response = { export const useSetupAddTokenFlow = ({ rejectToken: rejectTokenFn, addToken: addTokenFn, - assetCode: _assetCode, - assetIssuer: _assetIssuer, uuid, }: Params): Response => { const [isConfirming, setIsConfirming] = useState(false); diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index 11e904bd2d..d452e725c8 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -137,8 +137,6 @@ export const AddToken = () => { } = useSetupAddTokenFlow({ rejectToken, addToken, - assetCode, - assetIssuer, uuid, }); From 0b9b951deba2430b34622797dd9d73fffe702aa1 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 24 Jun 2026 15:52:06 -0300 Subject: [PATCH 05/14] add tests for sac and sep scenarios and adjust texts --- extension/e2e-tests/helpers/stubs.ts | 37 ++++ .../freighterApiIntegration.test.ts | 201 +++++++++++++++++- extension/src/popup/views/AddToken/index.tsx | 4 +- 3 files changed, 238 insertions(+), 4 deletions(-) diff --git a/extension/e2e-tests/helpers/stubs.ts b/extension/e2e-tests/helpers/stubs.ts index 28c174dc56..8d7f96ec80 100644 --- a/extension/e2e-tests/helpers/stubs.ts +++ b/extension/e2e-tests/helpers/stubs.ts @@ -3182,3 +3182,40 @@ export const stubMaintenanceBannerVariant = async ( }, }); }; + +/** + * Stubs the asset-list endpoint so that the given contractId appears as a + * verified token. This causes getVerifiedTokens() to return a non-empty array, + * which sets isVerifiedToken=true in AddToken and suppresses the + * "Not on your lists" AssetListWarning banner. + * + * Use page.route() (popup-level) for SEP-41 tokens and context.addInitScript() + * for the SAC flow (where the popup is created before page.route() can fire). + * + * @param page - Playwright Page or BrowserContext to attach the route to + * @param contractId - The contract address that should appear as verified + */ +export const stubVerifiedToken = async ( + page: Page | BrowserContext, + contractId: string, +) => { + const verifiedAssetList = { + ...STELLAR_EXPERT_ASSET_LIST_JSON, + assets: [ + ...STELLAR_EXPERT_ASSET_LIST_JSON.assets, + { + code: "E2E", + issuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + contract: contractId, + name: "E2E Token", + org: "unknown", + domain: "example.com", + decimals: 7, + }, + ], + }; + + await page.route("*/**/testnet/asset-list/**", async (route) => { + await route.fulfill({ json: verifiedAssetList }); + }); +}; diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index 13dd3eaaee..2698b954d6 100644 --- a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -17,6 +17,7 @@ import { stubScanTx, stubTokenDetails, stubTokenPrices, + stubVerifiedToken, } from "../helpers/stubs"; const TX_TO_SIGN = @@ -815,7 +816,7 @@ test("should not sign message when not allowed", async ({ await expect(popup.getByText("Connection Request")).toBeVisible(); }); -test("should add token when allowed", async ({ +test("should add an unverified SEP-41 token when allowed", async ({ page, extensionId, context, @@ -843,6 +844,7 @@ test("should add token when allowed", async ({ const popup = await popupPromise; await expect(popup.getByText("E2E Token")).toBeDefined(); + await expect(popup.getByText("Not on your lists")).toBeVisible(); await expectPageToHaveScreenshot({ page: popup, screenshot: "add-token.png", @@ -866,7 +868,7 @@ const SAC_CONTRACT_ID = // requires the window.fetch addInitScript approach described below. In // integration mode all stubs are bypassed and the real backend is used, but // the test account does not have a live SAC on testnet. -test("should open the Change Trust review and add a SAC token when allowed", async ({ +test("should add an unverified SAC token through the Change Trust review when allowed", async ({ page, extensionId, context, @@ -982,6 +984,7 @@ test("should open the Change Trust review and add a SAC token when allowed", asy await expect( popup.getByTestId("AddToken__Metadata__Row__TokenAddress"), ).toBeVisible(); + await expect(popup.getByText("Not on your lists")).toBeVisible(); // Clicking confirm for a SAC slides in the standard changeTrust review. await popup.getByTestId("add-token-approve").click(); @@ -992,7 +995,7 @@ test("should open the Change Trust review and add a SAC token when allowed", asy await expect(popup.getByText("Add Trustline")).toBeVisible(); }); -test("should not add token when not allowed", async ({ +test("should not add a SEP-41 token when the domain is not allowed", async ({ page, extensionId, context, @@ -1032,6 +1035,198 @@ test("should not add token when not allowed", async ({ }); }); +test("should add a verified SEP-41 token without the unverified banner", async ({ + page, + extensionId, + context, +}) => { + await stubIsSac(context); + // Stub the asset-list so TEST_TOKEN_ADDRESS appears as a verified token. + // This causes getVerifiedTokens() to find a match and setIsVerifiedToken(true), + // which suppresses the AssetListWarning "Not on your lists" banner. + await stubVerifiedToken(page, TEST_TOKEN_ADDRESS); + + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + await allowDapp({ page }); + + // open a second tab and go to docs playground + const pageTwo = await page.context().newPage(); + await pageTwo.waitForLoadState(); + + const popupPromise = page.context().waitForEvent("page"); + await pageTwo.goto( + "https://play.freighter.app/#/extension/playground/addToken", + ); + await pageTwo.getByRole("textbox").first().fill(TEST_TOKEN_ADDRESS); + await pageTwo + .getByRole("textbox") + .nth(1) + .fill("Test SDF Network ; September 2015"); + await pageTwo.getByText("Add Token").click(); + + const popup = await popupPromise; + + // Also stub the asset-list on the popup page itself (belt-and-suspenders for + // requests fired after the popup is captured). + await stubVerifiedToken(popup, TEST_TOKEN_ADDRESS); + + await expect(popup.getByText("E2E Token")).toBeDefined(); + // Verified token: the "Not on your lists" banner must NOT be shown. + await expect(popup.getByText("Not on your lists")).toHaveCount(0); + await popup.getByTestId("add-token-approve").click(); + + await expect(pageTwo.locator("#result-addToken")).toContainText( + "Token added:", + ); +}); + +// SAC verified token test: skipped in integration mode for the same reasons as the +// unverified SAC test — stub injection via addInitScript is not compatible with +// integration mode (real backend is used and there is no live SAC on testnet). +test("should add a verified SAC token through the Change Trust review without the unverified banner", async ({ + page, + extensionId, + context, +}) => { + test.skip( + isIntegrationMode, + "SAC stub injection via addInitScript is not compatible with integration mode", + ); + + // Inject the SAC stubs (is-sac-contract, token-details) AND the asset-list + // via window.fetch override so they fire before the popup page script runs. + // This is the same technique used by the unverified SAC test for is-sac-contract + // and token-details; here we extend it to also cover the asset-list fetch so + // that getVerifiedTokens() finds SAC_CONTRACT_ID and sets isVerifiedToken=true. + await context.addInitScript( + ({ + sacIssuer, + sacContractId, + }: { + sacIssuer: string; + sacContractId: string; + }) => { + const origFetch = (window as Window & typeof globalThis).fetch.bind( + window, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).fetch = function (input: any, init: any) { + const url: string = + typeof input === "string" + ? input + : input instanceof URL + ? input.href + : (input?.url ?? ""); + if (url.includes("/is-sac-contract/")) { + return Promise.resolve( + new Response(JSON.stringify({ isSacContract: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + } + if (url.includes("/token-details/")) { + return Promise.resolve( + new Response( + JSON.stringify({ + name: `E2E:${sacIssuer}`, + symbol: "E2E", + decimals: 7, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + if (url.includes("/asset-list/")) { + return Promise.resolve( + new Response( + JSON.stringify({ + name: "StellarExpert Top 50", + provider: "StellarExpert", + description: "Verified asset list", + version: "1.0", + network: "testnet", + feedback: "https://stellar.expert", + assets: [ + { + code: "E2E", + issuer: sacIssuer, + contract: sacContractId, + name: "E2E Token", + org: "unknown", + domain: "example.com", + decimals: 7, + }, + ], + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ), + ); + } + return origFetch(input, init); + }; + }, + { sacIssuer: SAC_ISSUER, sacContractId: SAC_CONTRACT_ID }, + ); + + // context.route() as belt-and-suspenders + await stubIsSacTrue(context); + await stubSacTokenDetails(context); + await stubVerifiedToken(context, SAC_CONTRACT_ID); + + await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); + await allowDapp({ page }); + + const pageTwo = await page.context().newPage(); + await pageTwo.waitForLoadState(); + + const popupPromise = page.context().waitForEvent("page"); + await pageTwo.goto( + "https://play.freighter.app/#/extension/playground/addToken", + ); + await pageTwo.getByRole("textbox").first().fill(SAC_CONTRACT_ID); + await pageTwo + .getByRole("textbox") + .nth(1) + .fill("Test SDF Network ; September 2015"); + await pageTwo.getByText("Add Token").click(); + + const popup = await popupPromise; + + // Belt-and-suspenders page-level stubs on the popup. + await stubIsSacTrue(popup); + await stubSacTokenDetails(popup); + await stubVerifiedToken(popup, SAC_CONTRACT_ID); + await stubFeeStats(popup); + await stubHorizonAccounts(popup); + await stubScanAssetSafe(popup); + await stubScanTx(popup); + await stubBackendSubmitTx(popup); + await stubAccountBalances(popup); + + await expect(popup.getByTestId("AddToken__Metadata__Row__Fee")).toBeVisible({ + timeout: 15000, + }); + await expect( + popup.getByTestId("AddToken__Metadata__Row__TokenAddress"), + ).toBeVisible(); + // Verified SAC token: the "Not on your lists" banner must NOT be shown. + await expect(popup.getByText("Not on your lists")).toHaveCount(0); + + await popup.getByTestId("add-token-approve").click(); + + await expect(popup.getByTestId("ChangeTrustInternal__Body")).toBeVisible({ + timeout: 15000, + }); + await expect(popup.getByText("Add Trustline")).toBeVisible(); +}); + test("should get public key when logged out", async ({ page, extensionId, diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index d452e725c8..a12d36a523 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -552,7 +552,9 @@ export const AddToken = () => { isSac ? setShowTrustlineReview(true) : handleApprove() } > - {t("Confirm")} + {/* SAC is a 2-step flow (Add Token → Change Trust review), so this + first button advances ("Continue"); SEP-41 confirms directly. */} + {isSac ? t("Continue") : t("Confirm")} From e00510aa7dcf8d642af8eeac6a06fdb2dfd82f7c Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Wed, 24 Jun 2026 18:06:05 -0300 Subject: [PATCH 06/14] adjust sac check for assets and adjust layouting --- .../ChangeTrustInternal/SubmitTx/index.tsx | 8 +++++++- .../ManageAssetRows/ChangeTrustInternal/index.tsx | 1 + .../ChangeTrustInternal/styles.scss | 14 +++++++++++--- .../views/AddToken/__tests__/AddToken.test.tsx | 10 ++++++++++ extension/src/popup/views/AddToken/index.tsx | 15 ++++++++++++--- 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx index bb34f54b2f..a5a81f1d1f 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx @@ -48,6 +48,7 @@ interface SubmitTransactionProps { icons: AssetIcons; goBack: () => void; onSuccess: () => void; + onClose: () => void; } export const SubmitTransaction = ({ @@ -57,6 +58,7 @@ export const SubmitTransaction = ({ fee, goBack, onSuccess, + onClose, }: SubmitTransactionProps) => { const { t } = useTranslation(); const dispatch: AppDispatch = useDispatch(); @@ -229,7 +231,11 @@ export const SubmitTransaction = ({ isHardwareWallet, isSuccess, }); - onSuccess(); + if (isSuccess) { + onSuccess(); + } else { + onClose(); + } }} > {t("Done")} diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 86d5ff537f..8dca2ab4d3 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -539,6 +539,7 @@ export const ChangeTrustInternal = ({ fee={fee} goBack={() => setActiveBodyContent(ActiveBodyContent.details)} onSuccess={onSuccess ?? onCancel} + onClose={onCancel} /> )}
diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss index 871ed13507..6862603fce 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss @@ -11,10 +11,15 @@ border-radius: 0.875rem; // Full popup view (external Add Token), vs the content-sized in-app modal: - // fill the height so actions/footer pin to the bottom instead of floating. + // fill the height so actions/footer pin to the bottom and the scroll region + // expands to occupy all the space down to the buttons (no gray gap). Use a + // hard height (not min-height) so the inner Body is firmly bounded and + // becomes the scroll container instead of staying content-sized. &--standalone { flex: 1; - min-height: 100%; + height: 100%; + min-height: 0; + overflow: hidden; // Grow the slider so the action row is pushed to the bottom. > .multi-pane-slider { @@ -31,9 +36,12 @@ } } - .multi-pane-slider__pane--active { + // The pane is the scroll container: bounded to the slider height and + // scrolls its content, so the details fill down to the pinned buttons. + .multi-pane-slider__pane { height: 100%; min-height: 0; + overflow: auto; } // Single scroll container, so transaction details scroll fully instead of diff --git a/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx b/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx index 217524ea37..1bf743404c 100644 --- a/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx +++ b/extension/src/popup/views/AddToken/__tests__/AddToken.test.tsx @@ -146,6 +146,16 @@ jest.mock("popup/helpers/useTokenLookup", () => { }; }); +// AddToken imports isContractId + isAssetSac from soroban. Stub both: a contract +// id is a "C..." string, and a token is a SAC when the resolved issuer is a +// classic G-address — matching the SAC vs SEP-41 setup driven by +// mockTokenLookupConfig.issuer above. +jest.mock("popup/helpers/soroban", () => ({ + isContractId: (id?: string) => typeof id === "string" && id.startsWith("C"), + isAssetSac: ({ asset }: { asset: { issuer?: string } }) => + (asset?.issuer || "").startsWith("G"), +})); + jest.mock("@shared/api/helpers/getIconUrlFromIssuer", () => ({ getIconUrlFromIssuer: jest.fn().mockResolvedValue(""), })); diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index a12d36a523..52cb0b3f9f 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -11,7 +11,7 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate, useLocation } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; -import { BASE_FEE, StellarToml, StrKey } from "stellar-sdk"; +import { BASE_FEE, StellarToml } from "stellar-sdk"; import { AppDispatch } from "popup/App"; import { METRIC_NAMES } from "popup/constants/metricsNames"; @@ -42,7 +42,7 @@ import { VerifyAccount } from "popup/views/VerifyAccount"; import { View } from "popup/basics/layout/View"; import { ManageAssetCurrency } from "popup/components/manageAssets/ManageAssetRows"; import { useTokenLookup } from "popup/helpers/useTokenLookup"; -import { isContractId } from "popup/helpers/soroban"; +import { isContractId, isAssetSac } from "popup/helpers/soroban"; import { scanAsset, isAssetSuspicious, @@ -110,7 +110,6 @@ export const AddToken = () => { const dispatch: AppDispatch = useDispatch(); const [showTrustlineReview, setShowTrustlineReview] = useState(false); - const isSac = StrKey.isValidEd25519PublicKey(assetIssuer); // `recommendedFee` is the network-recommended fee in XLM after useNetworkFees // resolves; pre-fetch it is the raw BASE_FEE (stroops), so guard against that @@ -312,6 +311,16 @@ export const AddToken = () => { const { networkPassphrase, networkName } = state.data.settings.networkDetails; + // Verify the contract is the derived SAC, not just a G-address issuer. + const isSac = isAssetSac({ + asset: { + code: assetCode, + issuer: assetIssuer, + contract: assetCurrency?.contract, + }, + networkDetails: state.data.settings.networkDetails, + }); + if (entryNetworkPassphrase && entryNetworkPassphrase !== networkPassphrase) { return ( Date: Thu, 25 Jun 2026 11:36:32 -0300 Subject: [PATCH 07/14] adjust hide close tab hint to avoid users leaving hanging promise --- .../integration-tests/freighterApiIntegration.test.ts | 7 +++---- .../ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx | 7 ++++++- .../ManageAssetRows/ChangeTrustInternal/index.tsx | 1 + 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts index 2698b954d6..5852fc610d 100644 --- a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts +++ b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts @@ -1041,10 +1041,9 @@ test("should add a verified SEP-41 token without the unverified banner", async ( context, }) => { await stubIsSac(context); - // Stub the asset-list so TEST_TOKEN_ADDRESS appears as a verified token. - // This causes getVerifiedTokens() to find a match and setIsVerifiedToken(true), - // which suppresses the AssetListWarning "Not on your lists" banner. - await stubVerifiedToken(page, TEST_TOKEN_ADDRESS); + // Mark verified (no "Not on your lists" banner). On `context` so it's set + // before the popup's initial asset-list fetch. + await stubVerifiedToken(context, TEST_TOKEN_ADDRESS); await loginToTestAccount({ page, extensionId, context, isIntegrationMode }); await allowDapp({ page }); diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx index a5a81f1d1f..67fe0e8ae9 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/SubmitTx/index.tsx @@ -49,6 +49,10 @@ interface SubmitTransactionProps { goBack: () => void; onSuccess: () => void; onClose: () => void; + // Hide the "close this tab" hint shown during submit: it nudges users to + // close and leave the dApp's addToken promise hanging. The transaction itself + // still succeeds even if they close. + hideCloseTabHint?: boolean; } export const SubmitTransaction = ({ @@ -59,6 +63,7 @@ export const SubmitTransaction = ({ goBack, onSuccess, onClose, + hideCloseTabHint = false, }: SubmitTransactionProps) => { const { t } = useTranslation(); const dispatch: AppDispatch = useDispatch(); @@ -177,7 +182,7 @@ export const SubmitTransaction = ({ - {isLoading && ( + {isLoading && !hideCloseTabHint && ( <>
{t( diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 8dca2ab4d3..735d45f54f 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -540,6 +540,7 @@ export const ChangeTrustInternal = ({ goBack={() => setActiveBodyContent(ActiveBodyContent.details)} onSuccess={onSuccess ?? onCancel} onClose={onCancel} + hideCloseTabHint={isFullHeight} /> )}
From de107fad101983e2955d267f8fbf8e85eb0fa7c5 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Thu, 25 Jun 2026 12:39:54 -0300 Subject: [PATCH 08/14] fix tests and scrollbar behavior on standalone pane --- .../add-token-chromium-darwin.png | Bin 24786 -> 24636 bytes .../ChangeTrustInternal/styles.scss | 11 +++++++++++ .../src/popup/views/AddToken/styles.scss | 1 - 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png b/extension/e2e-tests/integration-tests/freighterApiIntegration.test.ts-snapshots/add-token-chromium-darwin.png index b1d0d9db132424be5f278a48a9090f5338f03861..1890e0ecd2642dcfd8d0698a7a6c2a1ba5b36956 100644 GIT binary patch literal 24636 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 Cmk$yE literal 24786 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 Date: Thu, 25 Jun 2026 13:22:29 -0300 Subject: [PATCH 09/14] fix fee setting for transaction --- .../hooks/useChangeTrustData.tsx | 13 ++++++++++++- .../ManageAssetRows/ChangeTrustInternal/index.tsx | 2 +- .../ManageAssetRows/ChangeTrustInternal/styles.scss | 1 - 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/hooks/useChangeTrustData.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/hooks/useChangeTrustData.tsx index c29c86bd7f..cc8e38ff46 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/hooks/useChangeTrustData.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/hooks/useChangeTrustData.tsx @@ -122,7 +122,18 @@ function useGetChangeTrustData({ useEffect(() => { fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [ + addTrustline, + asset.code, + asset.contract, + asset.domain, + asset.issuer, + blockaidOverrideState, + networkDetails.networkPassphrase, + networkDetails.networkUrl, + publicKey, + recommendedFee, + ]); return { state, diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 735d45f54f..2317069e0b 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -101,7 +101,7 @@ export const ChangeTrustInternal = ({ addTrustline, publicKey, networkDetails, - recommendedFee: BASE_FEE, + recommendedFee: fee, }); useEffect(() => { diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss index 30e3bf35d6..3130aa1e77 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss @@ -66,7 +66,6 @@ .ChangeTrustInternal__Body__Wrapper { max-height: none; min-height: 0; - overflow-y: visible; } .ChangeTrustInternal__TransactionDetails { From 3c1c113e0ef7753f8324619b8ead1e140d71d061 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Thu, 25 Jun 2026 14:14:19 -0300 Subject: [PATCH 10/14] fix scroll behavior to auto on affected views --- .../ChangeTrustInternal/Settings/Fee/styles.scss | 7 ++++++- .../ChangeTrustInternal/Settings/Memo/styles.scss | 7 ++++++- .../ChangeTrustInternal/Settings/Timeout/styles.scss | 7 ++++++- .../ManageAssetRows/ChangeTrustInternal/styles.scss | 7 ++++++- extension/src/popup/views/SignTransaction/styles.scss | 7 ++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss index 0a36b1a5c7..9513f514b5 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss @@ -6,9 +6,14 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: scroll; + overflow-y: auto; max-width: 57rem; padding: pxToRem(8px); + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss index c0d2cb0927..a4e27adbee 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss @@ -6,9 +6,14 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: scroll; + overflow-y: auto; max-width: 57rem; padding: pxToRem(8px); + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss index 90cacda670..e39a5ede40 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss @@ -6,9 +6,14 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: scroll; + overflow-y: auto; max-width: 57rem; padding: pxToRem(8px); + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss index 3130aa1e77..4f09329b63 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss @@ -300,7 +300,12 @@ display: flex; flex-direction: column; width: 100%; - overflow-y: scroll; + overflow-y: auto; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } &__Header { display: flex; diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index c557316647..9c16d517f4 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -358,8 +358,13 @@ display: flex; flex-direction: column; width: 100%; - overflow-y: scroll; + overflow-y: auto; max-width: 57rem; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } &__Header { display: flex; From cd0ae16babe47c3c4e19743e02370ed009db9eb3 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 26 Jun 2026 10:28:05 -0300 Subject: [PATCH 11/14] revert styles override --- .../ChangeTrustInternal/Settings/Fee/styles.scss | 7 +------ .../ChangeTrustInternal/Settings/Memo/styles.scss | 7 +------ .../ChangeTrustInternal/Settings/Timeout/styles.scss | 7 +------ .../ManageAssetRows/ChangeTrustInternal/styles.scss | 7 +------ extension/src/popup/views/SignTransaction/styles.scss | 7 +------ 5 files changed, 5 insertions(+), 30 deletions(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss index 9513f514b5..0a36b1a5c7 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Fee/styles.scss @@ -6,14 +6,9 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: auto; + overflow-y: scroll; max-width: 57rem; padding: pxToRem(8px); - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss index a4e27adbee..c0d2cb0927 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Memo/styles.scss @@ -6,14 +6,9 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: auto; + overflow-y: scroll; max-width: 57rem; padding: pxToRem(8px); - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss index e39a5ede40..90cacda670 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/Settings/Timeout/styles.scss @@ -6,14 +6,9 @@ justify-content: space-between; width: 100%; height: 100%; - overflow-y: auto; + overflow-y: scroll; max-width: 57rem; padding: pxToRem(8px); - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } &__form { height: 100%; diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss index 4f09329b63..3130aa1e77 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/styles.scss @@ -300,12 +300,7 @@ display: flex; flex-direction: column; width: 100%; - overflow-y: auto; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } + overflow-y: scroll; &__Header { display: flex; diff --git a/extension/src/popup/views/SignTransaction/styles.scss b/extension/src/popup/views/SignTransaction/styles.scss index 9c16d517f4..c557316647 100644 --- a/extension/src/popup/views/SignTransaction/styles.scss +++ b/extension/src/popup/views/SignTransaction/styles.scss @@ -358,13 +358,8 @@ display: flex; flex-direction: column; width: 100%; - overflow-y: auto; + overflow-y: scroll; max-width: 57rem; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } &__Header { display: flex; From 6e59e6e88d5f199bba56dab5a4f2b2c5b599eea1 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 26 Jun 2026 10:32:42 -0300 Subject: [PATCH 12/14] side fix: adjust auto lock timer translations --- extension/src/popup/locales/en/translation.json | 2 +- extension/src/popup/locales/pt/translation.json | 4 ++-- extension/src/popup/views/AutoLockTimer/index.tsx | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 89f0be20e0..c334a8f01a 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -78,7 +78,7 @@ "Authorize": "Authorize", "Authorized address": "Authorized address", "Auto-lock timer": "Auto-lock timer", - "Auto-Lock Timer": "Auto-Lock Timer", + "Auto-Lock timer": "Auto-Lock timer", "available": "available", "Back": "Back", "Balance": "Balance", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 9b97c00d28..6fcc3e8759 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -77,8 +77,8 @@ "Authorizations": "Autorizações", "Authorize": "Autorizar", "Authorized address": "Endereço autorizado", - "Auto-lock timer": "Auto-lock timer", - "Auto-Lock Timer": "Auto-Lock Timer", + "Auto-lock timer": "Tempo para bloqueio automático", + "Auto-Lock timer": "Auto-Lock timer", "available": "disponível", "Back": "Voltar", "Balance": "Saldo", diff --git a/extension/src/popup/views/AutoLockTimer/index.tsx b/extension/src/popup/views/AutoLockTimer/index.tsx index 2d2af8a0b4..ff17864119 100644 --- a/extension/src/popup/views/AutoLockTimer/index.tsx +++ b/extension/src/popup/views/AutoLockTimer/index.tsx @@ -92,10 +92,8 @@ export const AutoLockTimer = () => { setIsSaving(true); await dispatch( saveSettings({ - isDataSharingAllowed: - settings.isDataSharingAllowed ?? false, - isMemoValidationEnabled: - settings.isMemoValidationEnabled ?? true, + isDataSharingAllowed: settings.isDataSharingAllowed ?? false, + isMemoValidationEnabled: settings.isMemoValidationEnabled ?? true, isHideDustEnabled: settings.isHideDustEnabled ?? true, isOpenSidebarByDefault: settings.isOpenSidebarByDefault ?? false, autoLockTimeoutMinutes: minutes, @@ -106,7 +104,7 @@ export const AutoLockTimer = () => { return ( - +
{VALID_AUTO_LOCK_TIMEOUT_MINUTES.map((minutes) => { From ca58f47426d44c33d12d59fb60956ddee02bbdfa Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 26 Jun 2026 10:36:54 -0300 Subject: [PATCH 13/14] side fix: adjust auto lock timer translations --- extension/src/popup/locales/en/translation.json | 1 - extension/src/popup/locales/pt/translation.json | 1 - extension/src/popup/views/AutoLockTimer/index.tsx | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index c334a8f01a..e6ee4aa0c0 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -78,7 +78,6 @@ "Authorize": "Authorize", "Authorized address": "Authorized address", "Auto-lock timer": "Auto-lock timer", - "Auto-Lock timer": "Auto-Lock timer", "available": "available", "Back": "Back", "Balance": "Balance", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 6fcc3e8759..af7209f1f9 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -78,7 +78,6 @@ "Authorize": "Autorizar", "Authorized address": "Endereço autorizado", "Auto-lock timer": "Tempo para bloqueio automático", - "Auto-Lock timer": "Auto-Lock timer", "available": "disponível", "Back": "Voltar", "Balance": "Saldo", diff --git a/extension/src/popup/views/AutoLockTimer/index.tsx b/extension/src/popup/views/AutoLockTimer/index.tsx index ff17864119..1c12a57542 100644 --- a/extension/src/popup/views/AutoLockTimer/index.tsx +++ b/extension/src/popup/views/AutoLockTimer/index.tsx @@ -104,7 +104,7 @@ export const AutoLockTimer = () => { return ( - +
{VALID_AUTO_LOCK_TIMEOUT_MINUTES.map((minutes) => { From 913fd30048b53b7fdf16fe0cec1bcf3c56e6aa43 Mon Sep 17 00:00:00 2001 From: leofelix077 Date: Fri, 26 Jun 2026 11:13:35 -0300 Subject: [PATCH 14/14] harden failed submit --- .../ManageAssetRows/ChangeTrustInternal/index.tsx | 6 +++++- extension/src/popup/views/AddToken/index.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx index 2317069e0b..87cc0cb071 100644 --- a/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx +++ b/extension/src/popup/components/manageAssets/ManageAssetRows/ChangeTrustInternal/index.tsx @@ -54,6 +54,9 @@ interface ChangeTrustInternalProps { addTrustline: boolean; onCancel: () => void; onSuccess?: () => void; + // Dismiss after a FAILED submit. Defaults to onCancel; the external Add Token + // flow passes a handler that rejects the dApp request so it doesn't hang. + onClose?: () => void; // Initial fee (in XLM) for the transaction. The external Add Token (SAC) // flow passes the network-recommended fee it already displayed so the // charged fee matches the disclosed one. Defaults to the base fee. @@ -71,6 +74,7 @@ export const ChangeTrustInternal = ({ networkDetails, onCancel, onSuccess, + onClose, initialFee, isFullHeight = false, }: ChangeTrustInternalProps) => { @@ -539,7 +543,7 @@ export const ChangeTrustInternal = ({ fee={fee} goBack={() => setActiveBodyContent(ActiveBodyContent.details)} onSuccess={onSuccess ?? onCancel} - onClose={onCancel} + onClose={onClose ?? onCancel} hideCloseTabHint={isFullHeight} /> )} diff --git a/extension/src/popup/views/AddToken/index.tsx b/extension/src/popup/views/AddToken/index.tsx index 52cb0b3f9f..5887c103f3 100644 --- a/extension/src/popup/views/AddToken/index.tsx +++ b/extension/src/popup/views/AddToken/index.tsx @@ -392,6 +392,7 @@ export const AddToken = () => { networkDetails={state.data.settings.networkDetails} onCancel={() => setShowTrustlineReview(false)} onSuccess={handleSacSuccess} + onClose={rejectAndClose} initialFee={displayFee} isFullHeight />