From 1adbc27bead5aa16249938b291127f349cbe9963 Mon Sep 17 00:00:00 2001 From: Emmet Townsend Date: Wed, 10 Jun 2026 19:04:50 +0100 Subject: [PATCH 1/3] fix: sort credential fields by display_order before rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Credentials component renders fields in the order returned by the API without sorting by display_order. When the API returns password before username (as observed with MX Bank), users see password first — a confusing UX. This sorts credentials by display_order so fields render in the intended sequence regardless of API response ordering. --- src/views/credentials/Credentials.js | 15 +++++++++------ .../credentials/__tests__/Credentials-test.tsx | 17 +++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/views/credentials/Credentials.js b/src/views/credentials/Credentials.js index 9c0e665a00..c8803549d7 100644 --- a/src/views/credentials/Credentials.js +++ b/src/views/credentials/Credentials.js @@ -112,9 +112,12 @@ export const Credentials = React.forwardRef( const tokens = useTokens() const styles = getStyles(tokens, isSmall) const getNextDelay = getDelay(0, 100) - const initialValues = buildInitialValues(credentials) - const formSchema = buildFormSchema(credentials) - const loginFieldCount = credentials.length + const sortedCredentials = [...credentials].sort( + (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0), + ) + const initialValues = buildInitialValues(sortedCredentials) + const formSchema = buildFormSchema(sortedCredentials) + const loginFieldCount = sortedCredentials.length const showDisconnectOption = currentMember && currentMember.is_managed_by_user && @@ -249,7 +252,7 @@ export const Credentials = React.forwardRef( const inputRefs = useRef({}) useEffect(() => { - for (const field of credentials) { + for (const field of sortedCredentials) { if (errors[field.field_name]) { inputRefs.current[field.field_name]?.focus() break @@ -258,7 +261,7 @@ export const Credentials = React.forwardRef( }, [errors]) function attemptConnect() { - const credentialsPayload = credentials.map((credential) => { + const credentialsPayload = sortedCredentials.map((credential) => { return { guid: credential.guid, value: values[credential.field_name], @@ -443,7 +446,7 @@ export const Credentials = React.forwardRef( onSubmit={(e) => e.preventDefault()} style={styles.form} > - {credentials.map((field) => ( + {sortedCredentials.map((field) => ( {field.field_type === CREDENTIAL_FIELD_TYPES.PASSWORD ? (
diff --git a/src/views/credentials/__tests__/Credentials-test.tsx b/src/views/credentials/__tests__/Credentials-test.tsx index 30b629f6fd..70e5618393 100644 --- a/src/views/credentials/__tests__/Credentials-test.tsx +++ b/src/views/credentials/__tests__/Credentials-test.tsx @@ -140,6 +140,23 @@ describe('Credentials', () => { expect(screen.getByText('Data access by')).toBeInTheDocument() }) }) + it('renders credentials in display_order regardless of API response order', async () => { + const reversedCredentialProps = { + ...credentialProps, + credentials: [ + { guid: 'CRD-456', label: 'Password', field_name: 'password', field_type: 1, display_order: 2 }, + { guid: 'CRD-123', label: 'Username', field_name: 'username', field_type: 3, display_order: 1 }, + ], + } + const ref = React.createRef() + render(, { + preloadedState: initialStateCopy, + }) + + const inputs = await screen.findAllByRole('textbox') + expect(inputs[0]).toHaveAccessibleName(/Username/i) + }) + it('renders credentials and makes sure that the powered by MX footer is not present', () => { const ref = React.createRef() render(, { preloadedState: initialStateCopy }) From 63bc8da3f1d5603506a285ef64ef9a0ed4c5794d Mon Sep 17 00:00:00 2001 From: Emmet Townsend Date: Wed, 10 Jun 2026 19:14:40 +0100 Subject: [PATCH 2/3] fix: default missing display_order to Infinity and improve test assertions - Use Infinity instead of 0 for missing display_order so credentials without the field sort to the end rather than the top - Query test fields by accessible label and assert DOM order instead of relying on array indexing of all textbox roles --- src/views/credentials/Credentials.js | 2 +- src/views/credentials/__tests__/Credentials-test.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/views/credentials/Credentials.js b/src/views/credentials/Credentials.js index c8803549d7..1c8d238dfb 100644 --- a/src/views/credentials/Credentials.js +++ b/src/views/credentials/Credentials.js @@ -113,7 +113,7 @@ export const Credentials = React.forwardRef( const styles = getStyles(tokens, isSmall) const getNextDelay = getDelay(0, 100) const sortedCredentials = [...credentials].sort( - (a, b) => (a.display_order ?? 0) - (b.display_order ?? 0), + (a, b) => (a.display_order ?? Infinity) - (b.display_order ?? Infinity), ) const initialValues = buildInitialValues(sortedCredentials) const formSchema = buildFormSchema(sortedCredentials) diff --git a/src/views/credentials/__tests__/Credentials-test.tsx b/src/views/credentials/__tests__/Credentials-test.tsx index 70e5618393..e945a21c68 100644 --- a/src/views/credentials/__tests__/Credentials-test.tsx +++ b/src/views/credentials/__tests__/Credentials-test.tsx @@ -153,8 +153,9 @@ describe('Credentials', () => { preloadedState: initialStateCopy, }) - const inputs = await screen.findAllByRole('textbox') - expect(inputs[0]).toHaveAccessibleName(/Username/i) + const usernameField = await screen.findByLabelText(/Enter your Username/i) + const passwordField = await screen.findByLabelText(/Password/i) + expect(usernameField.compareDocumentPosition(passwordField) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() }) it('renders credentials and makes sure that the powered by MX footer is not present', () => { From 14be0a0e714b9893d21661e6bd7684d36f7754bd Mon Sep 17 00:00:00 2001 From: Emmet Townsend Date: Wed, 10 Jun 2026 19:23:47 +0100 Subject: [PATCH 3/3] refactor: memoize sorted credentials and improve test coverage - Wrap sortedCredentials in useMemo to match codebase conventions - Format test file to stay within line length limits - Add test for credentials with missing display_order falling to end --- src/views/credentials/Credentials.js | 6 ++- .../__tests__/Credentials-test.tsx | 53 +++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/views/credentials/Credentials.js b/src/views/credentials/Credentials.js index 1c8d238dfb..3f23624cad 100644 --- a/src/views/credentials/Credentials.js +++ b/src/views/credentials/Credentials.js @@ -1,5 +1,6 @@ import React, { Fragment, + useMemo, useRef, useState, useEffect, @@ -112,8 +113,9 @@ export const Credentials = React.forwardRef( const tokens = useTokens() const styles = getStyles(tokens, isSmall) const getNextDelay = getDelay(0, 100) - const sortedCredentials = [...credentials].sort( - (a, b) => (a.display_order ?? Infinity) - (b.display_order ?? Infinity), + const sortedCredentials = useMemo( + () => [...credentials].sort((a, b) => (a.display_order ?? Infinity) - (b.display_order ?? Infinity)), + [credentials], ) const initialValues = buildInitialValues(sortedCredentials) const formSchema = buildFormSchema(sortedCredentials) diff --git a/src/views/credentials/__tests__/Credentials-test.tsx b/src/views/credentials/__tests__/Credentials-test.tsx index e945a21c68..546f2ad7f1 100644 --- a/src/views/credentials/__tests__/Credentials-test.tsx +++ b/src/views/credentials/__tests__/Credentials-test.tsx @@ -144,8 +144,20 @@ describe('Credentials', () => { const reversedCredentialProps = { ...credentialProps, credentials: [ - { guid: 'CRD-456', label: 'Password', field_name: 'password', field_type: 1, display_order: 2 }, - { guid: 'CRD-123', label: 'Username', field_name: 'username', field_type: 3, display_order: 1 }, + { + guid: 'CRD-456', + label: 'Password', + field_name: 'password', + field_type: 1, + display_order: 2, + }, + { + guid: 'CRD-123', + label: 'Username', + field_name: 'username', + field_type: 3, + display_order: 1, + }, ], } const ref = React.createRef() @@ -155,7 +167,42 @@ describe('Credentials', () => { const usernameField = await screen.findByLabelText(/Enter your Username/i) const passwordField = await screen.findByLabelText(/Password/i) - expect(usernameField.compareDocumentPosition(passwordField) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + const passwordFollowsUsername = + usernameField.compareDocumentPosition(passwordField) & + Node.DOCUMENT_POSITION_FOLLOWING + expect(passwordFollowsUsername).toBeTruthy() + }) + + it('sorts credentials without display_order to the end', async () => { + const mixedCredentialProps = { + ...credentialProps, + credentials: [ + { + guid: 'CRD-789', + label: 'PIN', + field_name: 'pin', + field_type: 3, + }, + { + guid: 'CRD-123', + label: 'Username', + field_name: 'username', + field_type: 3, + display_order: 1, + }, + ], + } + const ref = React.createRef() + render(, { + preloadedState: initialStateCopy, + }) + + const usernameField = await screen.findByLabelText(/Enter your Username/i) + const pinField = await screen.findByLabelText(/Enter your PIN/i) + const pinFollowsUsername = + usernameField.compareDocumentPosition(pinField) & + Node.DOCUMENT_POSITION_FOLLOWING + expect(pinFollowsUsername).toBeTruthy() }) it('renders credentials and makes sure that the powered by MX footer is not present', () => {