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
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ describe('MultichainAccountSelectorList', () => {
fireEvent.changeText(searchInput, searchTerm);
});

// Wait for debounce to complete and filtering to occur
// Check both visible and hidden items to ensure filtering has completed
// Wait for debounce (1s) to complete and filtering to occur. Use a
// generous timeout so CI has time for debounce + re-render + list update.
await waitFor(
() => {
expectedVisible.forEach((text) => {
Expand Down
84 changes: 0 additions & 84 deletions app/components/UI/Card/sdk/CardSDK.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,90 +624,6 @@ describe('CardSDK', () => {
});
});

describe('getGeoLocation', () => {
it('returns UNKNOWN when API call fails', async () => {
const error = new Error('Network error');
(global.fetch as jest.Mock).mockRejectedValueOnce(error);

const result = await cardSDK.getGeoLocation();

expect(result).toBe('UNKNOWN');
expect(Logger.error).toHaveBeenCalledWith(
error,
expect.objectContaining({
tags: expect.objectContaining({
feature: 'card',
operation: 'getGeoLocation',
}),
}),
);
});

it('returns UNKNOWN when fetch throws an error', async () => {
const fetchError = new Error('Fetch failed');
(global.fetch as jest.Mock).mockRejectedValueOnce(fetchError);

const result = await cardSDK.getGeoLocation();

expect(result).toBe('UNKNOWN');
expect(Logger.error).toHaveBeenCalledWith(
fetchError,
expect.objectContaining({
tags: expect.objectContaining({
feature: 'card',
operation: 'getGeoLocation',
}),
}),
);
});

it('returns UNKNOWN when response.text() throws an error', async () => {
const textError = new Error('Failed to read response text');
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: jest.fn().mockRejectedValue(textError),
});

const result = await cardSDK.getGeoLocation();

expect(result).toBe('UNKNOWN');
expect(Logger.error).toHaveBeenCalledWith(
textError,
expect.objectContaining({
tags: expect.objectContaining({
feature: 'card',
operation: 'getGeoLocation',
}),
}),
);
});

it('handles different country codes correctly', async () => {
const countryCodes = ['US', 'GB', 'CA', 'DE', 'FR', 'UNKNOWN'];

for (const code of countryCodes) {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValue(code),
});

const result = await cardSDK.getGeoLocation();
expect(result).toBe(code);
}
});

it('handles empty string response from API', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: jest.fn().mockResolvedValue(''),
});

const result = await cardSDK.getGeoLocation();

expect(result).toBe('');
});
});

describe('getSupportedTokensAllowances', () => {
const testAddress = '0x1234567890123456789012345678901234567890';

Expand Down
23 changes: 0 additions & 23 deletions app/components/UI/Card/sdk/CardSDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,29 +465,6 @@ export class CardSDK {
return batches;
}

getGeoLocation = async (): Promise<string> => {
try {
const response = await fetch(
'https://on-ramp.api.cx.metamask.io/geolocation',
);

if (!response.ok) {
throw new Error(`Failed to get geolocation: ${response.statusText}`);
}

return await response.text();
} catch (error) {
Logger.error(error as Error, {
tags: { feature: 'card', operation: 'getGeoLocation' },
context: {
name: 'card_geolocation',
data: { endpoint: 'geolocation' },
},
});
return 'UNKNOWN';
}
};

// Only runs on linea network
getSupportedTokensAllowances = async (
address: string,
Expand Down
2 changes: 0 additions & 2 deletions app/components/UI/Card/sdk/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ jest.mock('./CardSDK', () => ({
isCardEnabled: true,
getSupportedTokensByChainId: jest.fn(() => []),
isCardHolder: jest.fn(),
getGeoLocation: jest.fn(),
getSupportedTokensAllowances: jest.fn(),
getPriorityToken: jest.fn(),
refreshLocalToken: jest.fn(),
Expand Down Expand Up @@ -171,7 +170,6 @@ describe('CardSDK Context', () => {
isCardEnabled: true,
getSupportedTokensByChainId: jest.fn(() => []),
isCardHolder: jest.fn(),
getGeoLocation: jest.fn(),
getSupportedTokensAllowances: jest.fn(),
getPriorityToken: jest.fn(),
getRegistrationStatus: jest.fn(),
Expand Down
64 changes: 33 additions & 31 deletions app/components/UI/Card/util/getCardholder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import Logger from '../../../../util/Logger';
import { CardFeatureFlag } from '../../../../selectors/featureFlagController/card';
import { isValidHexAddress } from '../../../../util/address';

// Mock dependencies
jest.mock('../sdk/CardSDK');
jest.mock('../../../../util/Logger');
jest.mock('../../../../util/address');

const mockControllerMessengerCall = jest.fn();
jest.mock('../../../../core/Engine', () => ({
controllerMessenger: {
call: (...args: unknown[]) => mockControllerMessengerCall(...args),
},
}));

const MockedCardSDK = CardSDK as jest.MockedClass<typeof CardSDK>;
const mockedLogger = Logger as jest.Mocked<typeof Logger>;
const mockedIsValidHexAddress = isValidHexAddress as jest.MockedFunction<
Expand Down Expand Up @@ -54,16 +60,13 @@ describe('getCardholder', () => {

mockCardSDKInstance = {
isCardHolder: jest.fn(),
getGeoLocation: jest.fn(),
} as unknown as jest.Mocked<CardSDK>;

MockedCardSDK.mockImplementation(() => mockCardSDKInstance);

// Mock address utilities
mockedIsValidHexAddress.mockReturnValue(true);

// Default mock for geolocation
mockCardSDKInstance.getGeoLocation.mockResolvedValue('US');
mockControllerMessengerCall.mockResolvedValue('US');
});

describe('successful scenarios', () => {
Expand All @@ -74,7 +77,6 @@ describe('getCardholder', () => {
] as `${string}:${string}:${string}`[];

mockCardSDKInstance.isCardHolder.mockResolvedValue(mockResult);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('US');

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
Expand All @@ -94,16 +96,15 @@ describe('getCardholder', () => {
expect(mockCardSDKInstance.isCardHolder).toHaveBeenCalledWith(
mockFormattedAccounts,
);
expect(mockCardSDKInstance.getGeoLocation).toHaveBeenCalled();
});

it('should return only cardholder addresses from mixed results', async () => {
const mockResult = [
'eip155:59144:0x1234567890abcdef1234567890abcdef12345678',
] as `${string}:${string}:${string}`[];

mockControllerMessengerCall.mockResolvedValue('GB');
mockCardSDKInstance.isCardHolder.mockResolvedValue(mockResult);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('GB');

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
Expand All @@ -117,8 +118,8 @@ describe('getCardholder', () => {
});

it('should return empty array and geolocation when no accounts are cardholders', async () => {
mockControllerMessengerCall.mockResolvedValue('CA');
mockCardSDKInstance.isCardHolder.mockResolvedValue([]);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('CA');

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
Expand Down Expand Up @@ -310,8 +311,8 @@ describe('getCardholder', () => {
'eip155:59144:0x3333333333333333333333333333333333333333',
] as `${string}:${string}:${string}`[];

mockControllerMessengerCall.mockResolvedValue('DE');
mockCardSDKInstance.isCardHolder.mockResolvedValue(mockResult);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('DE');

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
Expand All @@ -335,8 +336,8 @@ describe('getCardholder', () => {
'also:invalid',
] as `${string}:${string}:${string}`[];

mockControllerMessengerCall.mockResolvedValue('FR');
mockCardSDKInstance.isCardHolder.mockResolvedValue(mockResult);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('FR');

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
Expand All @@ -356,8 +357,8 @@ describe('getCardholder', () => {
'eip155:59144:0x2222222222222222222222222222222222222222',
] as `${string}:${string}:${string}`[];

mockControllerMessengerCall.mockResolvedValue('ES');
mockCardSDKInstance.isCardHolder.mockResolvedValue(mockResult);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('ES');
mockedIsValidHexAddress
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
Expand All @@ -379,35 +380,36 @@ describe('getCardholder', () => {
});
});

describe('geolocation handling', () => {
it('should handle different geolocation values', async () => {
const geoLocations = ['US', 'GB', 'CA', 'DE', 'UNKNOWN'];

for (const geoLocation of geoLocations) {
mockCardSDKInstance.isCardHolder.mockResolvedValue([
'eip155:59144:0x1234567890abcdef1234567890abcdef12345678',
] as `${string}:${string}:${string}`[]);
mockCardSDKInstance.getGeoLocation.mockResolvedValue(geoLocation);
describe('geolocation from controller messenger', () => {
it('should await geolocation from GeolocationController:getGeolocation', async () => {
mockControllerMessengerCall.mockResolvedValue('JP');
mockCardSDKInstance.isCardHolder.mockResolvedValue([
'eip155:59144:0x1234567890abcdef1234567890abcdef12345678',
] as `${string}:${string}:${string}`[]);

const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
cardFeatureFlag: mockCardFeatureFlag,
});
const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
cardFeatureFlag: mockCardFeatureFlag,
});

expect(result.geoLocation).toBe(geoLocation);
}
expect(result.geoLocation).toBe('JP');
expect(mockControllerMessengerCall).toHaveBeenCalledWith(
'GeolocationController:getGeolocation',
);
});

it('should call getGeoLocation for each request', async () => {
it('should return UNKNOWN when getGeolocation rejects', async () => {
mockControllerMessengerCall.mockRejectedValue(
new Error('Controller unavailable'),
);
mockCardSDKInstance.isCardHolder.mockResolvedValue([]);
mockCardSDKInstance.getGeoLocation.mockResolvedValue('US');

await getCardholder({
const result = await getCardholder({
caipAccountIds: mockFormattedAccounts,
cardFeatureFlag: mockCardFeatureFlag,
});

expect(mockCardSDKInstance.getGeoLocation).toHaveBeenCalledTimes(1);
expect(result.geoLocation).toBe('UNKNOWN');
});
});
});
9 changes: 7 additions & 2 deletions app/components/UI/Card/util/getCardholder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CardSDK } from '../sdk/CardSDK';
import Logger from '../../../../util/Logger';
import { isValidHexAddress } from '../../../../util/address';
import { isCaipAccountId, parseCaipAccountId } from '@metamask/utils';
import Engine from '../../../../core/Engine';

export const getCardholder = async ({
caipAccountIds,
Expand All @@ -26,8 +27,12 @@ export const getCardholder = async ({
cardFeatureFlag,
});

const cardCaipAccountIds = await cardSDK.isCardHolder(caipAccountIds);
const geoLocation = await cardSDK.getGeoLocation();
const [cardCaipAccountIds, geoLocation] = await Promise.all([
cardSDK.isCardHolder(caipAccountIds),
Engine.controllerMessenger
.call('GeolocationController:getGeolocation')
.catch(() => 'UNKNOWN'),
]);

const cardholderAddresses = cardCaipAccountIds.map((cardCaipAccountId) => {
if (!isCaipAccountId(cardCaipAccountId)) return null;
Expand Down
Loading
Loading