diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 21e3b3c0a3e3..da76ff136052 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -98,7 +98,17 @@ function isDeviceLogin(login: NewLogin) { } function getDeviceLogins(logins: OnyxEntry) { - return Object.values(logins ?? {})?.filter(isDeviceLogin); + return Object.values(logins ?? {}) + ?.filter(isDeviceLogin) + .sort((a, b) => { + const aLastLogin = getLastLogin(a); + const bLastLogin = getLastLogin(b); + if (aLastLogin === bLastLogin) { + return 0; + } + // lastLogin/created are ISO datetime strings, so a lexicographic comparison sorts them chronologically. Descending puts the most recent device first. + return aLastLogin > bLastLogin ? -1 : 1; + }); } function hasDeviceManagementError(logins: OnyxEntry) { diff --git a/tests/unit/UserUtilsTest.ts b/tests/unit/UserUtilsTest.ts index ff6efc44f90d..396acd0c03e0 100644 --- a/tests/unit/UserUtilsTest.ts +++ b/tests/unit/UserUtilsTest.ts @@ -178,4 +178,73 @@ describe('UserUtils', () => { expect(Object.keys(result ?? {})).toEqual(['user@example.com']); }); }); + + describe('getDeviceLogins', () => { + test('returns an empty array when there are no logins', () => { + expect(UserUtils.getDeviceLogins(undefined)).toEqual([]); + }); + + test('sorts device logins by most recent timestamp first', () => { + const logins = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '14_oldest': { + created: '2024-01-01', + accountID: 1, + partnerID: CONST.PARTNER_ID.IPHONE, + partnerUserID: 'oldest@example.com', + lastLogin: '2024-01-02', + validatedDate: null, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '16_newest': { + created: '2024-01-01', + accountID: 1, + partnerID: CONST.PARTNER_ID.ANDROID, + partnerUserID: 'newest@example.com', + lastLogin: '2024-03-10', + validatedDate: null, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '83_middle': { + created: '2024-01-01', + accountID: 1, + partnerID: CONST.PARTNER_ID.NEWDOT, + partnerUserID: 'middle@example.com', + lastLogin: '2024-02-05', + validatedDate: null, + }, + }; + + const result = UserUtils.getDeviceLogins(logins); + + expect(result.map((login) => login.partnerUserID)).toEqual(['newest@example.com', 'middle@example.com', 'oldest@example.com']); + }); + + test('falls back to created when lastLogin is the default 2008 value', () => { + const logins = { + // eslint-disable-next-line @typescript-eslint/naming-convention + '14_a': { + created: '2024-01-01', + accountID: 1, + partnerID: CONST.PARTNER_ID.IPHONE, + partnerUserID: 'a@example.com', + lastLogin: '2008-01-01', + validatedDate: null, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '16_b': { + created: '2024-05-01', + accountID: 1, + partnerID: CONST.PARTNER_ID.ANDROID, + partnerUserID: 'b@example.com', + lastLogin: '2008-01-01', + validatedDate: null, + }, + }; + + const result = UserUtils.getDeviceLogins(logins); + + expect(result.map((login) => login.partnerUserID)).toEqual(['b@example.com', 'a@example.com']); + }); + }); });