diff --git a/__tests__/components/screens/WalletKit/DappMessageDisplay.test.tsx b/__tests__/components/screens/WalletKit/DappMessageDisplay.test.tsx new file mode 100644 index 000000000..3948ed817 --- /dev/null +++ b/__tests__/components/screens/WalletKit/DappMessageDisplay.test.tsx @@ -0,0 +1,65 @@ +import { DappMessageDisplay } from "components/screens/WalletKit/DappMessageDisplay"; +import { renderWithProviders } from "helpers/testUtils"; +import React from "react"; +import { Dimensions, StyleSheet } from "react-native"; + +jest.mock("hooks/useAppTranslation", () => ({ + __esModule: true, + default: () => ({ t: (key: string) => key }), +})); + +describe("DappMessageDisplay", () => { + it("renders the message content", () => { + const { getByTestId } = renderWithProviders( + , + ); + + expect(getByTestId("message-display-content")).toHaveTextContent( + "hello world", + ); + }); + + it("pretty-prints JSON messages", () => { + const { getByTestId } = renderWithProviders( + , + ); + + expect(getByTestId("message-display-content").props.children).toBe( + JSON.stringify({ a: 1 }, null, 2), + ); + }); + + it("bounds the message scroll area so long messages cannot push the action buttons off-screen", () => { + const { getByTestId } = renderWithProviders( + , + ); + + const scrollView = getByTestId("message-display-content-scroll"); + const style = StyleSheet.flatten(scrollView.props.style); + + expect(style.maxHeight).toBeDefined(); + expect(style.maxHeight).toBeLessThanOrEqual( + Dimensions.get("window").height * 0.5, + ); + }); + + it("can shrink below the cap (with a usable floor) when the sheet needs the room", () => { + // With a security banner + stacked warning buttons the sheet's fixed + // content grows; the message box must yield height so the action buttons + // stay on screen, while keeping a scrollable sliver of the message. + const { getByTestId } = renderWithProviders( + , + ); + + const boxStyle = StyleSheet.flatten( + getByTestId("message-display").props.style, + ); + const scrollStyle = StyleSheet.flatten( + getByTestId("message-display-content-scroll").props.style, + ); + + expect(boxStyle.flexShrink).toBe(1); + expect(scrollStyle.flexShrink).toBe(1); + expect(scrollStyle.minHeight).toBeGreaterThan(0); + }); +}); diff --git a/__tests__/helpers/walletKitValidation.test.ts b/__tests__/helpers/walletKitValidation.test.ts index 8ecb0ca6b..6d663bda3 100644 --- a/__tests__/helpers/walletKitValidation.test.ts +++ b/__tests__/helpers/walletKitValidation.test.ts @@ -148,13 +148,22 @@ describe("validateSignMessageLength", () => { } }); - it("returns valid for a message exactly at the 1 KB limit", () => { + it("allows a multi-KB JSON payload (limit is 10 KB, not 1 KB)", () => { + // Regression for stellar/freighter-mobile#957: dApps sign JSON payloads + // that routinely exceed 1 KB. SEP-53 imposes no size limit. + expect(SIGN_MESSAGE_MAX_BYTES).toBe(10240); + const message = JSON.stringify({ data: "a".repeat(4000) }); + const result = validateSignMessageLength(message); + expect(result.valid).toBe(true); + }); + + it("returns valid for a message exactly at the limit", () => { const message = "a".repeat(SIGN_MESSAGE_MAX_BYTES); const result = validateSignMessageLength(message); expect(result.valid).toBe(true); }); - it("returns error for a message exceeding 1 KB (ASCII)", () => { + it("returns error for a message exceeding the limit (ASCII)", () => { const message = "a".repeat(SIGN_MESSAGE_MAX_BYTES + 1); const result = validateSignMessageLength(message); expect(result.valid).toBe(false); @@ -163,9 +172,9 @@ describe("validateSignMessageLength", () => { } }); - it("returns error for a message exceeding 1 KB due to multi-byte UTF-8 chars", () => { - // Each emoji is 4 bytes in UTF-8 β€” 257 emojis = 1028 bytes > 1024 - const message = "πŸš€".repeat(257); + it("returns error for a message exceeding the limit due to multi-byte UTF-8 chars", () => { + // Each emoji is 4 bytes in UTF-8, so this is 4 bytes over the limit + const message = "πŸš€".repeat(SIGN_MESSAGE_MAX_BYTES / 4 + 1); const result = validateSignMessageLength(message); expect(result.valid).toBe(false); if (!result.valid) { @@ -173,9 +182,9 @@ describe("validateSignMessageLength", () => { } }); - it("returns valid for multi-byte chars that stay within 1 KB", () => { - // 256 emojis = 1024 bytes β€” exactly at the limit - const message = "πŸš€".repeat(256); + it("returns valid for multi-byte chars that stay within the limit", () => { + // Each emoji is 4 bytes in UTF-8 β€” exactly at the limit + const message = "πŸš€".repeat(SIGN_MESSAGE_MAX_BYTES / 4); const result = validateSignMessageLength(message); expect(result.valid).toBe(true); }); diff --git a/src/components/screens/WalletKit/DappMessageDisplay.tsx b/src/components/screens/WalletKit/DappMessageDisplay.tsx index 8f794c45f..700923b24 100644 --- a/src/components/screens/WalletKit/DappMessageDisplay.tsx +++ b/src/components/screens/WalletKit/DappMessageDisplay.tsx @@ -3,7 +3,7 @@ import { Text } from "components/sds/Typography"; import useColors from "hooks/useColors"; import React from "react"; import { useTranslation } from "react-i18next"; -import { ScrollView, View } from "react-native"; +import { Dimensions, ScrollView, View } from "react-native"; /** * Props for the DappMessageDisplay component @@ -28,7 +28,7 @@ const isJsonString = (str: string): boolean => { /** * DappMessageDisplay component for showing SEP-53 messages * Displays the message with the SEP-53 prefix and handles JSON formatting - * Dynamically increases height based on message length + * Grows with the message up to a capped height, then scrolls * * @component * @param {DappMessageDisplayProps} props - The component props @@ -49,7 +49,13 @@ export const DappMessageDisplay: React.FC = ({ return ( @@ -63,7 +69,19 @@ export const DappMessageDisplay: React.FC = ({ {t("common.message")} - + { const { themeColors } = useColors(); const { t } = useAppTranslation(); + const { height: windowHeight } = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const maxContentHeight = + windowHeight - insets.top - insets.bottom - SHEET_CHROME_HEIGHT; const accountList = useMemo( () => [ @@ -89,6 +102,7 @@ export const DappSignMessageBottomSheetContent: React.FC< return ( @@ -112,7 +126,9 @@ export const DappSignMessageBottomSheetContent: React.FC< securityWarningAction={securityWarningAction} /> - + {/* flexShrink lets the message box inside absorb the squeeze when the + sheet is taller than the screen; the account list keeps its size. */} + diff --git a/src/helpers/walletKitValidation.ts b/src/helpers/walletKitValidation.ts index b5458b7c9..afd981918 100644 --- a/src/helpers/walletKitValidation.ts +++ b/src/helpers/walletKitValidation.ts @@ -16,8 +16,13 @@ export const ValidationErrorKeys = { AUTH_ENTRY_ADDRESS_MISMATCH: "walletKit.errorAuthEntryAddressMismatch", } as const; -/** Max UTF-8 byte length for sign_message content per SEP-53. */ -export const SIGN_MESSAGE_MAX_BYTES = 1024; +/** + * Max UTF-8 byte length for sign_message content. SEP-53 imposes no size + * limit (and the browser extension enforces none) β€” this is a sanity cap + * against absurd WalletConnect payloads, sized to comfortably fit the JSON + * payloads dApps actually sign (see stellar/freighter-mobile#957). + */ +export const SIGN_MESSAGE_MAX_BYTES = 10240; // ───────────────────────────────────────────────────────────────────────────── // Types @@ -46,7 +51,7 @@ export function validateSignMessageContent( } /** - * Validates sign_message length: max 1KB UTF-8 bytes per SEP-53. + * Validates sign_message length against SIGN_MESSAGE_MAX_BYTES (UTF-8 bytes). */ export function validateSignMessageLength( message: string, diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index af0e19741..89df5d691 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -895,7 +895,7 @@ "errorAuthEntryNetworkMismatch": "Authorization entry is for a different network", "errorAuthEntryAddressMismatch": "Authorization entry is bound to a different account", "errorEmptyMessage": "Cannot sign empty message", - "errorMessageTooLong": "Message too long (max 1KB)", + "errorMessageTooLong": "Message too long (max 10KB)", "errorSubmitting": "Failed to submit transaction", "errorRespondingRequest": "Failed to respond transaction request", "errorWrongNetwork": "Wrong Network", diff --git a/src/i18n/locales/pt/translations.json b/src/i18n/locales/pt/translations.json index 3f15659c3..5291d6478 100644 --- a/src/i18n/locales/pt/translations.json +++ b/src/i18n/locales/pt/translations.json @@ -858,7 +858,7 @@ "errorAuthEntryNetworkMismatch": "A entrada de autorizaΓ§Γ£o Γ© para uma rede diferente", "errorAuthEntryAddressMismatch": "A entrada de autorizaΓ§Γ£o estΓ‘ vinculada a uma conta diferente", "errorEmptyMessage": "NΓ£o Γ© possΓ­vel assinar mensagem vazia", - "errorMessageTooLong": "Mensagem muito longa (mΓ‘ximo 1KB)", + "errorMessageTooLong": "Mensagem muito longa (mΓ‘ximo 10KB)", "errorSubmitting": "Erro ao enviar a transaΓ§Γ£o", "errorRespondingRequest": "Erro ao responder Γ  solicitaΓ§Γ£o de transaΓ§Γ£o", "errorWrongNetwork": "Rede incorreta", diff --git a/src/providers/WalletKitProvider.tsx b/src/providers/WalletKitProvider.tsx index c216380de..20666ff4f 100644 --- a/src/providers/WalletKitProvider.tsx +++ b/src/providers/WalletKitProvider.tsx @@ -724,7 +724,7 @@ export const WalletKitProvider: React.FC = ({ return false; } - // Step 2: Validate message length (1KB limit per SEP-53) + // Step 2: Validate message length (sanity cap; SEP-53 imposes no limit) const lengthResult = validateSignMessageLength(contentResult.value); if (!lengthResult.valid) { showToast({