Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3690,6 +3690,9 @@ const CONST = {
NAME: 'expensifyCard',
BANK: 'Expensify Card',
ROUTE: 'expensify-card',
CARD_PROGRAM: {
CURRENT: 'CURRENT',
},
FRAUD_TYPES: {
DOMAIN: 'domain',
INDIVIDUAL: 'individual',
Expand Down
23 changes: 19 additions & 4 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,11 +1084,26 @@ function getCardSettings(cardSettings: OnyxEntry<ExpensifyCardSettings>, 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;
Expand Down
6 changes: 6 additions & 0 deletions src/types/onyx/ExpensifyCardSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 48 additions & 14 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3245,6 +3245,8 @@ describe('CardUtils', () => {
} as ExpensifyCardSettings;

const nestedSettings = {
domainName: 'example.com',
preferredPolicy: 'policyID',
paymentBankAccountID: 12345,
limit: 50000,
US: {
Expand All @@ -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');
});
});
});

Expand Down
Loading