diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 60a350410ce31..71d4f165a8870 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3690,6 +3690,9 @@ const CONST = { NAME: 'expensifyCard', BANK: 'Expensify Card', ROUTE: 'expensify-card', + CARD_PROGRAM: { + CURRENT: 'CURRENT', + }, FRAUD_TYPES: { DOMAIN: 'domain', INDIVIDUAL: 'individual', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 9456622a03033..8199f8da27f69 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1084,11 +1084,26 @@ function getCardSettings(cardSettings: OnyxEntry, feedCou return undefined; } - if (feedCountry) { - const feedCountryCardSettings = cardSettings[feedCountry as keyof typeof cardSettings]; - if (feedCountryCardSettings && typeof feedCountryCardSettings === 'object' && !Array.isArray(feedCountryCardSettings)) { - return feedCountryCardSettings as ExpensifyCardSettingsBase; + const getMergedProgramSettings = (programKey: string): ExpensifyCardSettingsBase | undefined => { + const programSettings = cardSettings[programKey as keyof typeof cardSettings]; + if (programSettings && typeof programSettings === 'object' && !Array.isArray(programSettings)) { + // Nested program values take precedence — they are the authoritative source for + // program-specific fields once the backend sends the full nested format (Phase 2). + return {...cardSettings, ...(programSettings as ExpensifyCardSettingsBase)} as ExpensifyCardSettingsBase; } + return undefined; + }; + + if (feedCountry) { + return getMergedProgramSettings(feedCountry) ?? cardSettings; + } + + // Auto-detect: try known card programs in priority order so callers that + // don't pass feedCountry still get the right program sub-object when the + // backend sends nested settings (Phase 2 of fixing shared Onyx key). + const result = getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? getMergedProgramSettings(CONST.COUNTRY.GB); + if (result) { + return result; } return cardSettings; diff --git a/src/types/onyx/ExpensifyCardSettings.ts b/src/types/onyx/ExpensifyCardSettings.ts index 62c39a29926d3..db4f9fae25e93 100644 --- a/src/types/onyx/ExpensifyCardSettings.ts +++ b/src/types/onyx/ExpensifyCardSettings.ts @@ -72,6 +72,12 @@ type ExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback< /** Nested Expensify Card settings keyed by feed country from backend */ // eslint-disable-next-line @typescript-eslint/naming-convention US?: ExpensifyCardSettingsBase; + /** Nested settings for pre-2024 US card program from backend */ + // eslint-disable-next-line @typescript-eslint/naming-convention + CURRENT?: ExpensifyCardSettingsBase; + /** Nested settings for UK/EU card program from backend */ + // eslint-disable-next-line @typescript-eslint/naming-convention + GB?: ExpensifyCardSettingsBase; /** Nested Travel Invoicing settings from backend */ // eslint-disable-next-line @typescript-eslint/naming-convention TRAVEL_US?: ExpensifyCardSettingsBase; diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 26c0436397be0..7a8865856ee58 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -3245,6 +3245,8 @@ describe('CardUtils', () => { } as ExpensifyCardSettings; const nestedSettings = { + domainName: 'example.com', + preferredPolicy: 'policyID', paymentBankAccountID: 12345, limit: 50000, US: { @@ -3268,42 +3270,74 @@ describe('CardUtils', () => { expect(getCardSettings(null as unknown as undefined)).toBeUndefined(); }); - it('should return flat root when feedCountry is not provided', () => { + it('should return flat root when feedCountry is not provided and no nested keys exist', () => { const result = getCardSettings(flatSettings); expect(result).toBe(flatSettings); }); - it('should return flat root when feedCountry is undefined', () => { + it('should return flat root when feedCountry is undefined and no nested keys exist', () => { const result = getCardSettings(flatSettings, undefined); expect(result).toBe(flatSettings); }); - it('should return nested object when feedCountry matches a nested key', () => { + it('should return merged root + nested when feedCountry matches a nested key', () => { const result = getCardSettings(nestedSettings, 'US'); - expect(result).toEqual({ - paymentBankAccountID: 67890, - limit: 30000, - currentBalance: 500, - }); + expect(result?.paymentBankAccountID).toBe(67890); + expect(result?.limit).toBe(30000); + expect(result?.currentBalance).toBe(500); + expect(result?.domainName).toBe('example.com'); }); - it('should fall back to flat root when feedCountry key does not exist', () => { + it('should fall back to root when feedCountry key does not exist', () => { const result = getCardSettings(nestedSettings, 'CA'); expect(result).toBe(nestedSettings); }); - it('should return TRAVEL_US nested settings when feedCountry is TRAVEL_US', () => { + it('should return merged root + TRAVEL_US when feedCountry is TRAVEL_US', () => { const result = getCardSettings(nestedSettings, 'TRAVEL_US'); - expect(result).toEqual({ - paymentBankAccountID: 11111, - isEnabled: true, - }); + expect(result?.paymentBankAccountID).toBe(11111); + expect(result?.isEnabled).toBe(true); + expect(result?.domainName).toBe('example.com'); }); it('should not return primitive values as nested settings', () => { const result = getCardSettings(nestedSettings, 'limit'); expect(result).toBe(nestedSettings); }); + + it('should auto-detect US program when no feedCountry is provided', () => { + const result = getCardSettings(nestedSettings); + expect(result?.paymentBankAccountID).toBe(67890); + expect(result?.limit).toBe(30000); + expect(result?.currentBalance).toBe(500); + expect(result?.domainName).toBe('example.com'); + }); + + it('should auto-detect GB program when only GB nested key exists', () => { + const gbOnlySettings = { + domainName: 'uk-example.com', + GB: { + paymentBankAccountID: 99999, + limit: 20000, + }, + } as ExpensifyCardSettings; + const result = getCardSettings(gbOnlySettings); + expect(result?.paymentBankAccountID).toBe(99999); + expect(result?.domainName).toBe('uk-example.com'); + }); + + it('should auto-detect CURRENT program for legacy pre-2024 nested format', () => { + const currentSettings = { + domainName: 'legacy.com', + CURRENT: { + paymentBankAccountID: 55555, + limit: 10000, + }, + } as ExpensifyCardSettings; + const result = getCardSettings(currentSettings); + expect(result?.paymentBankAccountID).toBe(55555); + expect(result?.domainName).toBe('legacy.com'); + }); }); });