From 85bae01f1c65a8cc438ee236818c60ba239f79c3 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 11:48:26 -0400 Subject: [PATCH 1/6] Show all operation fields in the transaction signing details view The signing-approval Details view dropped several operation fields whose values are falsy but meaningful, and rendered combined account-flag bitmasks as blank. Use presence checks instead of truthy guards, decode flag bitmasks bit-by-bit, surface data-entry deletions and home-domain clears, and add a warning when a setOptions op disables the master key. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Operations/__tests__/index.test.tsx | 220 ++++++++++++++++++ .../signTransaction/Operations/index.tsx | 105 ++++++--- .../src/popup/locales/en/translation.json | 4 + .../src/popup/locales/pt/translation.json | 4 + 4 files changed, 305 insertions(+), 28 deletions(-) create mode 100644 extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx diff --git a/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx b/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx new file mode 100644 index 0000000000..358089d71e --- /dev/null +++ b/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { Provider } from "react-redux"; +import { + Account, + Asset, + BASE_FEE, + Networks, + Operation, + StrKey, + TransactionBuilder, + xdr, +} from "stellar-sdk"; + +import { makeDummyStore } from "popup/__testHelpers__"; +import { Operations } from "../index"; + +// setOptions never triggers the asset scanner, but mock it so the component's +// effect can never reach the network in the test environment. +jest.mock("popup/helpers/blockaid", () => ({ + scanAsset: jest.fn().mockResolvedValue(undefined), +})); + +// Build a setOptions transaction with the bundled SDK, serialize it to XDR and +// decode it back — the exact path Freighter uses to obtain the operation object +// it renders on the signing-approval screen. A present-but-zero Uint32 field +// (e.g. masterWeight: 0) decodes to the JS number 0. +// Valid ed25519 strkeys minted from fixed bytes — avoids curve math (and the +// crypto RNG, which is unavailable in this test environment). +const SOURCE_KEY = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 1)); +const ADDED_SIGNER = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 7)); + +type SetOptionsOptions = Parameters[0]; + +const decodeOperation = (operation: xdr.Operation) => { + const account = new Account(SOURCE_KEY, "0"); + const tx = new TransactionBuilder(account, { + fee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + }) + .addOperation(operation) + .setTimeout(0) + .build(); + + return TransactionBuilder.fromXDR(tx.toXDR(), Networks.TESTNET) + .operations as Operation[]; +}; + +const decodeSetOptions = (options: SetOptionsOptions) => + decodeOperation(Operation.setOptions(options)); + +const renderOps = (operations: Operation[]) => + render( + + + , + ); + +// Read the value rendered next to a given operation-detail label. +const rowValue = (key: string) => { + const keyEl = screen + .getAllByTestId("OperationKeyVal__key") + .find((el) => el.textContent === key); + return keyEl?.parentElement + ?.querySelector('[data-testid="OperationKeyVal__value"]') + ?.textContent?.trim(); +}; + +const MASTER_KEY_WARNING = /disables your account's master key/i; + +describe("Operations — setOptions field visibility", () => { + it("decoder yields numeric 0 (falsy) for masterWeight/thresholds", () => { + const [op] = decodeSetOptions({ + masterWeight: 0, + lowThreshold: 0, + medThreshold: 0, + highThreshold: 0, + }) as any[]; + + expect(op.masterWeight).toBe(0); + expect(op.lowThreshold).toBe(0); + expect(op.medThreshold).toBe(0); + expect(op.highThreshold).toBe(0); + }); + + it("renders masterWeight 0, zeroed thresholds, and a signer change, with a master-key warning", () => { + renderOps( + decodeSetOptions({ + masterWeight: 0, + lowThreshold: 0, + medThreshold: 0, + highThreshold: 0, + signer: { ed25519PublicKey: ADDED_SIGNER, weight: 1 }, + }), + ); + + expect(screen.getByText("Set Options")).toBeInTheDocument(); + expect(screen.getByText("Signer")).toBeInTheDocument(); + expect(rowValue("Master Weight")).toBe("0"); + expect(rowValue("High Threshold")).toBe("0"); + expect(rowValue("Medium Threshold")).toBe("0"); + expect(rowValue("Low Threshold")).toBe("0"); + expect(screen.getByText(MASTER_KEY_WARNING)).toBeInTheDocument(); + }); + + it("renders masterWeight 0 on its own with the warning, never an empty operation", () => { + renderOps(decodeSetOptions({ masterWeight: 0 })); + + expect(rowValue("Master Weight")).toBe("0"); + expect(screen.getByText(MASTER_KEY_WARNING)).toBeInTheDocument(); + }); + + it("non-zero masterWeight/threshold render and do not warn", () => { + renderOps(decodeSetOptions({ masterWeight: 2, highThreshold: 3 })); + + expect(rowValue("Master Weight")).toBe("2"); + expect(rowValue("High Threshold")).toBe("3"); + expect(screen.queryByText(MASTER_KEY_WARNING)).not.toBeInTheDocument(); + }); + + it("HOME DOMAIN: clearing the home domain is surfaced, not hidden", () => { + renderOps(decodeSetOptions({ homeDomain: "" })); + + expect(rowValue("Home Domain")).toBe("(clearing home domain)"); + }); + + it("FLAGS: a single-bit setFlags decodes to its label", () => { + renderOps(decodeSetOptions({ setFlags: 1 })); + + expect(rowValue("Set Flags")).toBe("Authorization Required"); + }); + + it("FLAGS: a combined setFlags bitmask decodes every set bit, not a blank value", () => { + // REVOCABLE (2) | CLAWBACK (8) = 10. The SDK types setFlags as a single + // AuthFlag, but the wire format is a bitmask — cast to exercise that. + renderOps(decodeSetOptions({ setFlags: 10 as any })); + + expect(rowValue("Set Flags")).toBe( + "Authorization Revocable, Authorization Clawback Enabled", + ); + }); + + it("FLAGS: a combined clearFlags bitmask decodes every set bit", () => { + // REQUIRED (1) | REVOCABLE (2) = 3 + renderOps(decodeSetOptions({ clearFlags: 3 as any })); + + expect(rowValue("Clear Flags")).toBe( + "Authorization Required, Authorization Revocable", + ); + }); +}); + +describe("Operations — manageData value visibility", () => { + it("renders a set value", () => { + renderOps( + decodeOperation(Operation.manageData({ name: "k", value: "hi" })), + ); + + expect(rowValue("Value")).toBe("hi"); + }); + + it("renders the Value row for an empty value rather than hiding it", () => { + renderOps(decodeOperation(Operation.manageData({ name: "k", value: "" }))); + + expect(screen.getByText("Value")).toBeInTheDocument(); + expect(rowValue("Value")).toBe(""); + }); + + it("surfaces a deletion when value is absent ", () => { + renderOps( + decodeOperation(Operation.manageData({ name: "k", value: null })), + ); + + expect(rowValue("Value")).toBe("(deleting entry)"); + }); +}); + +describe("Operations — setTrustLineFlags visibility", () => { + const TRUSTOR = StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 3)); + const ASSET = new Asset( + "USDC", + StrKey.encodeEd25519PublicKey(Buffer.alloc(32, 9)), + ); + + const decodeSetTrustLineFlags = (flags: { + authorized?: boolean; + authorizedToMaintainLiabilities?: boolean; + clawbackEnabled?: boolean; + }) => + decodeOperation( + Operation.setTrustLineFlags({ trustor: TRUSTOR, asset: ASSET, flags }), + ); + + it("renders a flag being enabled", () => { + renderOps(decodeSetTrustLineFlags({ authorized: true })); + + expect(rowValue("Authorized")).toBe("Enabled"); + }); + + it("renders a flag being cleared (set to false), not hidden", () => { + renderOps(decodeSetTrustLineFlags({ authorized: false })); + + expect(rowValue("Authorized")).toBe("Disabled"); + }); + + it("does not render a flag that is left unchanged", () => { + renderOps(decodeSetTrustLineFlags({ clawbackEnabled: false })); + + expect( + screen + .getAllByTestId("OperationKeyVal__key") + .some((el) => el.textContent === "Authorized"), + ).toBe(false); + expect(rowValue("Clawback Enabled")).toBe("Disabled"); + }); +}); diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index 57d55dde33..2c6a0b9d06 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -52,6 +52,26 @@ const MemoRequiredWarning = ({ ) : null; }; +const MasterKeyDisableWarning = () => { + const { t } = useTranslation(); + + return ( + } + variant="error" + /> + } + /> + ); +}; + const DestinationWarning = ({ destination, flaggedKeys, @@ -96,6 +116,18 @@ export const Operations = ({ "8": "Authorization Clawback Enabled", }; + // Account flags are a bitmask, so a combined value (e.g. REVOCABLE | + // CLAWBACK = 10) is not a key in AuthorizationMapToDisplay. Decode each set + // bit individually so combined flags are never rendered as a blank value. + const decodeAuthorizationFlags = (bits: number) => { + const labels = Object.entries(AuthorizationMapToDisplay) + .filter(([bit]) => (bits & Number(bit)) !== 0) + .map(([, label]) => label); + return labels.length + ? labels.join(", ") + : t("Unknown ({{bits}})", { bits }); + }; + const RenderOpByType = ({ op }: { op: Operation }) => { const networkDetails = useSelector(settingsNetworkDetailsSelector); @@ -337,48 +369,49 @@ export const Operations = ({ operationValue={inflationDest} /> )} - {homeDomain && ( + {homeDomain !== undefined && ( )} - {highThreshold && ( + {highThreshold !== undefined && ( )} - {medThreshold && ( + {medThreshold !== undefined && ( )} - {lowThreshold && ( + {lowThreshold !== undefined && ( )} - {masterWeight && ( + {masterWeight !== undefined && ( )} - {setFlags && ( + {masterWeight === 0 && } + {setFlags !== undefined && ( )} - {clearFlags && ( + {clearFlags !== undefined && ( )} @@ -435,15 +468,19 @@ export const Operations = ({ case "manageData": { const { name, value } = op; + // A null/undefined value means the data entry is being deleted; an + // empty value decodes to a zero-length buffer. Always render the row so + // a deletion is never silently hidden from the approval screen. + const isDeletingEntry = value === undefined || value === null; return ( <> - {value && ( - - )} + ); } @@ -537,22 +574,34 @@ export const Operations = ({ operationKey={t("Asset Code")} operationValue={asset.code} /> - {flags.authorized && ( + {/* + A flag present in the decoded `flags` object is being changed: + `true` enables it, `false` *clears* it. Use a presence check so a + cleared flag is never hidden, and render the value explicitly — a + raw boolean is not rendered by React. + */} + {flags.authorized !== undefined && ( )} - {flags.authorizedToMaintainLiabilities && ( + {flags.authorizedToMaintainLiabilities !== undefined && ( )} - {flags.clawbackEnabled && ( + {flags.clawbackEnabled !== undefined && ( )} diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index 9dad7219f6..c105eb1ccb 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -1,4 +1,6 @@ { + "(clearing home domain)": "(clearing home domain)", + "(deleting entry)": "(deleting entry)", "{{domain}} is not currently connected to Freighter": "{{domain}} is not currently connected to Freighter", "* All Stellar accounts must maintain a minimum balance of lumens.": "* All Stellar accounts must maintain a minimum balance of lumens.", "* payment methods may vary based on your location": "* payment methods may vary based on your location", @@ -621,6 +623,7 @@ "This site was flagged as malicious": "This site was flagged as malicious", "This token does not support muxed address (M-) as a target destination.": "This token does not support muxed address (M-) as a target destination.", "This transaction could not be completed.": "This transaction could not be completed.", + "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.": "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.", "This transaction does not appear safe for the following reasons": "This transaction does not appear safe for the following reasons", "This transaction does not appear safe for the following reasons.": "This transaction does not appear safe for the following reasons.", "This transaction is expected to fail": "This transaction is expected to fail", @@ -677,6 +680,7 @@ "Unable to scan transaction": "Unable to scan transaction", "Unable to sign out": "Unable to sign out", "Unexpected Error": "Unexpected Error", + "Unknown ({{bits}})": "Unknown ({{bits}})", "Unknown error occured": "Unknown error occured", "Unlock": "Unlock", "Unsupported signing method": "Unsupported signing method", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 90dd818068..6cf0c25306 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -1,4 +1,6 @@ { + "(clearing home domain)": "(clearing home domain)", + "(deleting entry)": "(deleting entry)", "{{domain}} is not currently connected to Freighter": "{{domain}} não está atualmente conectado ao Freighter", "* All Stellar accounts must maintain a minimum balance of lumens.": "* Todas as contas Stellar devem manter um saldo mínimo de lumens.", "* payment methods may vary based on your location": "* Os métodos de pagamento podem variar com base na sua localização", @@ -621,6 +623,7 @@ "This site was flagged as malicious": "Este site foi marcado como malicioso", "This token does not support muxed address (M-) as a target destination.": "Este token não suporta endereço muxed (M-) como destino.", "This transaction could not be completed.": "Esta transação não pôde ser concluída.", + "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.": "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.", "This transaction does not appear safe for the following reasons": "Esta transação não parece segura pelos seguintes motivos", "This transaction does not appear safe for the following reasons.": "Esta transação não parece segura pelos seguintes motivos.", "This transaction is expected to fail": "Esta transação deve falhar", @@ -677,6 +680,7 @@ "Unable to scan transaction": "Não foi possível verificar a transação", "Unable to sign out": "Não foi possível sair", "Unexpected Error": "Erro Inesperado", + "Unknown ({{bits}})": "Unknown ({{bits}})", "Unknown error occured": "Erro desconhecido ocorreu", "Unlock": "Desbloquear", "Unsupported signing method": "Método de assinatura não suportado", From fc632c8ee6613ab4eaa1e504d15a68144482d82d Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 11:52:18 -0400 Subject: [PATCH 2/6] Localize master-key warning altText for screen readers Use t("Warning") for the IconButton accessibility text so non-English locales don't get an English-only string. Addresses PR review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/popup/components/signTransaction/Operations/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index 2c6a0b9d06..e5e76b4b8f 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -63,7 +63,7 @@ const MasterKeyDisableWarning = () => { label={t( "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.", )} - altText="Warning" + altText={t("Warning")} icon={} variant="error" /> From a5a23995cb61daa31ce7e517f1a942704083fa73 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 12:28:28 -0400 Subject: [PATCH 3/6] Render master-key warning as a full-width wrapping banner The warning was a KeyValueList row, so its text landed in the right-aligned value column with white-space:nowrap and was truncated. Render it as a dedicated full-width banner (alert icon + wrapping text, role="alert") so the full message is always readable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../signTransaction/Operations/index.tsx | 27 ++++++++++--------- .../signTransaction/Operations/styles.scss | 26 ++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index e5e76b4b8f..4640d66eef 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -55,20 +55,21 @@ const MemoRequiredWarning = ({ const MasterKeyDisableWarning = () => { const { t } = useTranslation(); + // Rendered as a full-width banner (not a KeyValueList row) so the message + // wraps instead of being truncated in the right-aligned value column. return ( - } - variant="error" - /> - } - /> +
+
); }; diff --git a/extension/src/popup/components/signTransaction/Operations/styles.scss b/extension/src/popup/components/signTransaction/Operations/styles.scss index 63c4d71a3a..efdb7d595a 100644 --- a/extension/src/popup/components/signTransaction/Operations/styles.scss +++ b/extension/src/popup/components/signTransaction/Operations/styles.scss @@ -135,4 +135,30 @@ flex-direction: column; } } + + &__warning { + display: flex; + align-items: flex-start; + gap: pxToRem(8px); + margin-top: pxToRem(4px); + padding: pxToRem(12px); + border-radius: pxToRem(12px); + line-height: 1.5rem; + color: var(--sds-clr-red-11); + background-color: var(--sds-clr-red-03); + border: 1px solid var(--sds-clr-red-06); + + svg { + flex-shrink: 0; + width: pxToRem(16px); + height: pxToRem(16px); + margin-top: pxToRem(4px); + } + + span { + min-width: 0; + white-space: normal; + overflow-wrap: anywhere; + } + } } From 61bec6a94fbf14db0f2da15235d68280345580e6 Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 12:34:18 -0400 Subject: [PATCH 4/6] Add Portuguese translations for new sign-flow strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pt locale was showing the master-key warning, home-domain-clear and data-entry-delete placeholders, and the unknown-flags fallback verbatim in English. Provide pt-BR translations consistent with existing terms (Domínio Principal, Assinante, Peso Mestre). Co-Authored-By: Claude Opus 4.8 (1M context) --- extension/src/popup/locales/pt/translation.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 6cf0c25306..7f73757d9d 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -1,6 +1,6 @@ { - "(clearing home domain)": "(clearing home domain)", - "(deleting entry)": "(deleting entry)", + "(clearing home domain)": "(limpando o Domínio Principal)", + "(deleting entry)": "(excluindo entrada)", "{{domain}} is not currently connected to Freighter": "{{domain}} não está atualmente conectado ao Freighter", "* All Stellar accounts must maintain a minimum balance of lumens.": "* Todas as contas Stellar devem manter um saldo mínimo de lumens.", "* payment methods may vary based on your location": "* Os métodos de pagamento podem variar com base na sua localização", @@ -623,7 +623,7 @@ "This site was flagged as malicious": "Este site foi marcado como malicioso", "This token does not support muxed address (M-) as a target destination.": "Este token não suporta endereço muxed (M-) como destino.", "This transaction could not be completed.": "Esta transação não pôde ser concluída.", - "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.": "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.", + "This transaction disables your account's master key. You may permanently lose access to this account unless another signer with sufficient weight is added.": "Esta transação desativa a chave mestra da sua conta. Você pode perder permanentemente o acesso a esta conta, a menos que outro assinante com peso suficiente seja adicionado.", "This transaction does not appear safe for the following reasons": "Esta transação não parece segura pelos seguintes motivos", "This transaction does not appear safe for the following reasons.": "Esta transação não parece segura pelos seguintes motivos.", "This transaction is expected to fail": "Esta transação deve falhar", @@ -680,7 +680,7 @@ "Unable to scan transaction": "Não foi possível verificar a transação", "Unable to sign out": "Não foi possível sair", "Unexpected Error": "Erro Inesperado", - "Unknown ({{bits}})": "Unknown ({{bits}})", + "Unknown ({{bits}})": "Desconhecido ({{bits}})", "Unknown error occured": "Erro desconhecido ocorreu", "Unlock": "Desbloquear", "Unsupported signing method": "Método de assinatura não suportado", From 5b3312ea77a3070a1b43586ffd746f42ffb1bbfd Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 13:44:23 -0400 Subject: [PATCH 5/6] Localize decoded authorization flag labels decodeAuthorizationFlags returned the raw English labels from AuthorizationMapToDisplay; wrap them in t() and register the four flag labels in the en/pt locales so combined setFlags/clearFlags render translated in non-English locales. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/popup/components/signTransaction/Operations/index.tsx | 2 +- extension/src/popup/locales/en/translation.json | 4 ++++ extension/src/popup/locales/pt/translation.json | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index 4640d66eef..d1bc094541 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -123,7 +123,7 @@ export const Operations = ({ const decodeAuthorizationFlags = (bits: number) => { const labels = Object.entries(AuthorizationMapToDisplay) .filter(([bit]) => (bits & Number(bit)) !== 0) - .map(([, label]) => label); + .map(([, label]) => t(label)); return labels.length ? labels.join(", ") : t("Unknown ({{bits}})", { bits }); diff --git a/extension/src/popup/locales/en/translation.json b/extension/src/popup/locales/en/translation.json index c105eb1ccb..f3a09ce425 100644 --- a/extension/src/popup/locales/en/translation.json +++ b/extension/src/popup/locales/en/translation.json @@ -76,6 +76,10 @@ "Asset not found": "Asset not found", "asset options": "asset options", "At the end of this process, Freighter will only display accounts related to the new backup phrase.": "At the end of this process, Freighter will only display accounts related to the new backup phrase.", + "Authorization Clawback Enabled": "Authorization Clawback Enabled", + "Authorization Immutable": "Authorization Immutable", + "Authorization Required": "Authorization Required", + "Authorization Revocable": "Authorization Revocable", "Authorizations": "Authorizations", "Authorize": "Authorize", "available": "available", diff --git a/extension/src/popup/locales/pt/translation.json b/extension/src/popup/locales/pt/translation.json index 7f73757d9d..9d3608b0de 100644 --- a/extension/src/popup/locales/pt/translation.json +++ b/extension/src/popup/locales/pt/translation.json @@ -76,6 +76,10 @@ "Asset not found": "Ativo não encontrado", "asset options": "opções de ativo", "At the end of this process, Freighter will only display accounts related to the new backup phrase.": "No final deste processo, o Freighter exibirá apenas contas relacionadas à nova frase de backup.", + "Authorization Clawback Enabled": "Recuperação de Autorização Habilitada (Clawback)", + "Authorization Immutable": "Autorização Imutável", + "Authorization Required": "Autorização Obrigatória", + "Authorization Revocable": "Autorização Revogável", "Authorizations": "Autorizações", "Authorize": "Autorizar", "available": "disponível", From 8d2febe444b943eb392db3f782ab9ed2dc004a4f Mon Sep 17 00:00:00 2001 From: Piyal Basu Date: Fri, 5 Jun 2026 13:48:41 -0400 Subject: [PATCH 6/6] Surface unrecognized authorization flag bits alongside known ones decodeAuthorizationFlags returned only known labels when any known bit was present, hiding unrecognized bits combined with a known flag (e.g. 1 | 16). Track remaining bits and append an Unknown ({{bits}}) entry for them so no flag change is silently dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Operations/__tests__/index.test.tsx | 10 ++++++++ .../signTransaction/Operations/index.tsx | 25 +++++++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx b/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx index 358089d71e..e8090d12a7 100644 --- a/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx +++ b/extension/src/popup/components/signTransaction/Operations/__tests__/index.test.tsx @@ -144,6 +144,16 @@ describe("Operations — setOptions field visibility", () => { ); }); + it("FLAGS: a known bit combined with an unrecognized bit surfaces both", () => { + // REQUIRED (1) | future-bit (16) = 17 — the unknown bit must not be hidden. + renderOps(decodeSetOptions({ setFlags: 17 as any })); + + // The mocked t() does not interpolate, so {{bits}} stays literal here. + expect(rowValue("Set Flags")).toBe( + "Authorization Required, Unknown ({{bits}})", + ); + }); + it("FLAGS: a combined clearFlags bitmask decodes every set bit", () => { // REQUIRED (1) | REVOCABLE (2) = 3 renderOps(decodeSetOptions({ clearFlags: 3 as any })); diff --git a/extension/src/popup/components/signTransaction/Operations/index.tsx b/extension/src/popup/components/signTransaction/Operations/index.tsx index d1bc094541..96f1cc1495 100644 --- a/extension/src/popup/components/signTransaction/Operations/index.tsx +++ b/extension/src/popup/components/signTransaction/Operations/index.tsx @@ -118,15 +118,24 @@ export const Operations = ({ }; // Account flags are a bitmask, so a combined value (e.g. REVOCABLE | - // CLAWBACK = 10) is not a key in AuthorizationMapToDisplay. Decode each set - // bit individually so combined flags are never rendered as a blank value. + // CLAWBACK = 10) is not a key in AuthorizationMapToDisplay. Decode each known + // bit individually so combined flags are never rendered as a blank value, and + // surface any remaining (unrecognized) bits so a future protocol flag isn't + // silently hidden when combined with a known one. const decodeAuthorizationFlags = (bits: number) => { - const labels = Object.entries(AuthorizationMapToDisplay) - .filter(([bit]) => (bits & Number(bit)) !== 0) - .map(([, label]) => t(label)); - return labels.length - ? labels.join(", ") - : t("Unknown ({{bits}})", { bits }); + const labels: string[] = []; + let remaining = bits; + Object.entries(AuthorizationMapToDisplay).forEach(([bit, label]) => { + const value = Number(bit); + if ((bits & value) !== 0) { + labels.push(t(label)); + remaining &= ~value; + } + }); + if (remaining !== 0) { + labels.push(t("Unknown ({{bits}})", { bits: remaining })); + } + return labels.join(", "); }; const RenderOpByType = ({ op }: { op: Operation }) => {