Skip to content
Open
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
4 changes: 4 additions & 0 deletions .js.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ export OVERRIDE_REMOTE_FEATURE_FLAGS="false"
#Bridge flag to toggle between dev and prod API
export BRIDGE_USE_DEV_APIS="false"

# Point AuthenticationController + every JWT-consuming service at non-prod
# backends. One of: dev | prod (default: prod). Local dev only.
export MM_DEV_API_ENV="prod"

# Set ramps environment to staging.
# This is required when developing or checking existing ramps tests locally.
export RAMP_INTERNAL_BUILD="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import IdentityDeveloperOptionsSection from './IdentityDeveloperOptionsSection';
import Engine from '../../../../core/Engine';
import Logger from '../../../../util/Logger';

jest.mock('../../../../core/Engine', () => ({
__esModule: true,
default: {
context: {
AuthenticationController: {
performSignOut: jest.fn(),
},
},
},
}));

jest.mock('../../../../util/Logger', () => ({
__esModule: true,
default: {
error: jest.fn(),
},
}));

const mockPerformSignOut = Engine.context.AuthenticationController
.performSignOut as jest.Mock;
const mockLoggerError = Logger.error as jest.Mock;
const CLEAR_AUTH_SESSION_TEST_ID = 'identity-dev-clear-auth-session-button';

describe('IdentityDeveloperOptionsSection', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders the Identity heading', () => {
const { getByText } = renderWithProvider(
<IdentityDeveloperOptionsSection />,
);

expect(getByText('Identity')).toBeDefined();
});

it('surfaces the current MM_DEV_API_ENV in the description', () => {
const { getByText } = renderWithProvider(
<IdentityDeveloperOptionsSection />,
);

expect(getByText(/Current MM_DEV_API_ENV: prod/)).toBeDefined();
});

it('calls performSignOut when the clear button is pressed', () => {
const { getByTestId } = renderWithProvider(
<IdentityDeveloperOptionsSection />,
);

fireEvent.press(getByTestId(CLEAR_AUTH_SESSION_TEST_ID));

expect(mockPerformSignOut).toHaveBeenCalledTimes(1);
expect(mockLoggerError).not.toHaveBeenCalled();
});

it('logs via Logger.error and does not propagate when performSignOut throws', () => {
const failure = new Error('sign-out blew up');
mockPerformSignOut.mockImplementationOnce(() => {
throw failure;
});

const { getByTestId } = renderWithProvider(
<IdentityDeveloperOptionsSection />,
);

expect(() =>
fireEvent.press(getByTestId(CLEAR_AUTH_SESSION_TEST_ID)),
).not.toThrow();

expect(mockLoggerError).toHaveBeenCalledTimes(1);
expect(mockLoggerError).toHaveBeenCalledWith(
failure,
'IdentityDeveloperOptionsSection: clear auth session failed',
);
});

it('wraps non-Error throws before passing them to Logger.error', () => {
mockPerformSignOut.mockImplementationOnce(() => {
throw 'boom';
});

const { getByTestId } = renderWithProvider(
<IdentityDeveloperOptionsSection />,
);

fireEvent.press(getByTestId(CLEAR_AUTH_SESSION_TEST_ID));

expect(mockLoggerError).toHaveBeenCalledTimes(1);
const [loggedError, loggedContext] = mockLoggerError.mock.calls[0];
expect(loggedError).toBeInstanceOf(Error);
expect((loggedError as Error).message).toBe('boom');
expect(loggedContext).toBe(
'IdentityDeveloperOptionsSection: clear auth session failed',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useCallback } from 'react';
import {
Button,
ButtonVariant,
ButtonSize,
Text,
TextVariant,
TextColor,
} from '@metamask/design-system-react-native';
import { strings } from '../../../../../locales/i18n';
import { useTheme } from '../../../../util/theme';
import { useStyles } from '../../../../component-library/hooks';
import Engine from '../../../../core/Engine';
import Logger from '../../../../util/Logger';
import { devApiEnv } from '../../../../core/devApiEnv';
import styleSheet from './DeveloperOptions.styles';

const CLEAR_AUTH_SESSION_TEST_ID = 'identity-dev-clear-auth-session-button';

const IdentityDeveloperOptionsSection = () => {
const theme = useTheme();
const { styles } = useStyles(styleSheet, { theme });

const handleClearAuthSession = useCallback(() => {
try {
Engine.context.AuthenticationController.performSignOut();
} catch (error) {
Logger.error(
error instanceof Error ? error : new Error(String(error)),
'IdentityDeveloperOptionsSection: clear auth session failed',
);
}
}, []);

return (
<>
<Text
color={TextColor.TextDefault}
variant={TextVariant.HeadingLg}
style={styles.heading}
>
{strings('app_settings.developer_options.identity.title')}
</Text>
<Text
color={TextColor.TextAlternative}
variant={TextVariant.BodyMd}
style={styles.desc}
>
{strings('app_settings.developer_options.identity.description', {
env: devApiEnv(),
})}
</Text>
<Button
variant={ButtonVariant.Secondary}
size={ButtonSize.Lg}
onPress={handleClearAuthSession}
isFullWidth
style={styles.accessory}
testID={CLEAR_AUTH_SESSION_TEST_ID}
>
{strings(
'app_settings.developer_options.identity.clear_auth_session_button',
)}
</Button>
</>
);
};

export default IdentityDeveloperOptionsSection;
2 changes: 2 additions & 0 deletions app/components/Views/Settings/DeveloperOptions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useStyles } from '../../../../component-library/hooks';
import styleSheet from './DeveloperOptions.styles';
import SentryTest from './SentryTest';
import HapticsDeveloperOptionsSection from './HapticsDeveloperOptionsSection';
import IdentityDeveloperOptionsSection from './IdentityDeveloperOptionsSection';
///: BEGIN:ONLY_INCLUDE_IF(sample-feature)
import SampleFeatureDevSettingsEntryPoint from '../../../../features/SampleFeature/components/views/SampleFeatureDevSettingsEntryPoint/SampleFeatureDevSettingsEntryPoint';
///: END:ONLY_INCLUDE_IF
Expand Down Expand Up @@ -68,6 +69,7 @@ const DeveloperOptions = () => {
{isMusdConversionEnabled && <MusdDeveloperOptionsSection />}
{isMoneyHomeEnabled && <MoneyUiDeveloperOptionsSection />}
<CardDeveloperOptionsSection />
<IdentityDeveloperOptionsSection />
<HapticsDeveloperOptionsSection />
</ScrollView>
);
Expand Down
3 changes: 2 additions & 1 deletion app/constants/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BRIDGE_PROD_API_BASE_URL,
} from '@metamask/bridge-controller';
import { NETWORK_CHAIN_ID } from '../util/networks/customNetworks';
import { isDevApiEnv } from '../core/devApiEnv';

/**
* Native token address (zero address)
Expand Down Expand Up @@ -38,7 +39,7 @@ export const NETWORK_TO_SHORT_NETWORK_NAME_MAP: Record<
};

export const BRIDGE_API_BASE_URL =
process.env.BRIDGE_USE_DEV_APIS === 'true'
isDevApiEnv() || process.env.BRIDGE_USE_DEV_APIS === 'true'
? BRIDGE_DEV_API_BASE_URL
: BRIDGE_PROD_API_BASE_URL;

Expand Down
8 changes: 7 additions & 1 deletion app/controllers/perps/constants/perpsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* UI-only constants (layout, display, navigation) live in:
* app/components/UI/Perps/constants/perpsConfig.ts
*/
import { apiUrl } from '../../../core/devApiEnv';

export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ZERO_BALANCE = '0x0';

Expand Down Expand Up @@ -292,13 +294,17 @@
FallbackMaxLeverage: 50,
} as const;

// TODO: Confirm the perps dev-api hostname. The cx.metamask.io convention is

Check warning on line 297 in app/controllers/perps/constants/perpsConfig.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ4dRXdrnKuPtIgc5-aT&open=AZ4dRXdrnKuPtIgc5-aT&pullRequest=30055
// `*.api.cx.metamask.io` -> `*.dev-api.cx.metamask.io`; if perps deviates,
// replace `apiUrl(...)` below with an explicit dev URL switch.

/**
* Data Lake API configuration
* Endpoints for reporting perps trading activity for notifications
*/
export const DATA_LAKE_API_CONFIG = {
// Order reporting endpoint - only used for mainnet perps trading
OrdersEndpoint: 'https://perps.api.cx.metamask.io/api/v1/orders',
OrdersEndpoint: apiUrl('https://perps.api.cx.metamask.io/api/v1/orders'),
} as const;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
initMessenger: AssetsControllerInitMessenger,
): QueryApiClient {
if (!apiClient) {
// TODO(MM_DEV_API_ENV): `createApiPlatformClient` does not accept a URL or

Check warning on line 73 in app/core/Engine/controllers/assets-controller/assets-controller-init.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ4dRXGtnKuPtIgc5-aR&open=AZ4dRXGtnKuPtIgc5-aR&pullRequest=30055
// env override — `API_URLS` (accounts/prices/token/tokens) are sealed
// inside `@metamask/core-backend`. When `MM_DEV_API_ENV=dev`, these calls
// will still hit prod and 401 against a dev JWT. Needs upstream support
// for an `apiBaseUrl` / `env` option.
apiClient = createApiPlatformClient({
clientProduct: 'metamask-mobile',
getBearerToken: () => safeGetBearerToken(initMessenger),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,24 @@ describe('authenticatedUserStorageServiceInit', () => {
});

describe('getAuthenticatedUserStorageEnvironment', () => {
// The environment is pinned to `'prod'` to match `AuthenticationController`,
// which always mints PRD tokens on mobile. `METAMASK_ENVIRONMENT` therefore
// has no effect on this resolver — these cases document that contract so a
// future refactor doesn't silently reintroduce the token/env mismatch that
// caused 403 "invalid access token" responses.
const originalEnv = process.env.METAMASK_ENVIRONMENT;

// Tracks `MM_DEV_API_ENV` so it agrees with the env the auth controller
// mints JWTs for — a PRD token will 403 against dev user-storage and
// vice versa.
afterEach(() => {
process.env.METAMASK_ENVIRONMENT = originalEnv;
delete process.env.MM_DEV_API_ENV;
});

it.each(['production', 'beta', 'rc', 'dev', 'exp', 'test', 'e2e', 'unknown'])(
'returns prod regardless of METAMASK_ENVIRONMENT=%s',
(value) => {
process.env.METAMASK_ENVIRONMENT = value;
expect(getAuthenticatedUserStorageEnvironment()).toBe('prod');
},
);
it('returns prod when MM_DEV_API_ENV is unset', () => {
expect(getAuthenticatedUserStorageEnvironment()).toBe('prod');
});

it.each(['dev', 'prod'] as const)('tracks MM_DEV_API_ENV=%s', (value) => {
process.env.MM_DEV_API_ENV = value;
expect(getAuthenticatedUserStorageEnvironment()).toBe(value);
});

it('returns prod when METAMASK_ENVIRONMENT is unset', () => {
delete process.env.METAMASK_ENVIRONMENT;
it('falls back to prod for unrecognized MM_DEV_API_ENV values', () => {
process.env.MM_DEV_API_ENV = 'nonsense';
expect(getAuthenticatedUserStorageEnvironment()).toBe('prod');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,15 @@ import {
} from '@metamask/authenticated-user-storage';
import type { MessengerClientInitFunction } from '../types';
import Logger from '../../../util/Logger';
import { devApiEnv } from '../../devApiEnv';

/**
* Returns the environment to use for the authenticated-user-storage service.
*
* The environment MUST match the one used by `AuthenticationController`, which
* on mobile is always `Env.PRD` (see `authentication-controller-init.ts` — no
* `config.env` override is passed, so it falls back to the controller's
* default of `Env.PRD`). Mobile (and extension) only ever mint PRD bearer
* tokens from the OIDC token endpoint.
*
* Pointing user-storage at `dev` / `uat` on a non-production build would cause
* every request to 403 with "invalid access token" because a PRD-issued token
* cannot be validated by the dev/uat user-storage APIs. This mirrors the fix
* already applied in `profile-metrics-service-init.ts`, which also hardcodes
* `Env.PRD` for the same reason.
*
* @returns Always `'prod'`.
* The environment MUST match the one used by `AuthenticationController`: a
* PRD-issued JWT cannot be validated against dev user-storage APIs and
* vice versa. Both read from the same `devApiEnv` source so they always agree.
*/
export function getAuthenticatedUserStorageEnvironment(): Environment {
return 'prod';
return devApiEnv();
}

/**
Expand Down
25 changes: 25 additions & 0 deletions app/core/Engine/controllers/chomp-api-service-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,29 @@ describe('chompApiServiceInit', () => {
{ fallback: 'https://chomp.dev-api.cx.metamask.io' },
);
});

describe('when MM_DEV_API_ENV=dev', () => {
beforeEach(() => {
process.env.MM_DEV_API_ENV = 'dev';
});

afterEach(() => {
delete process.env.MM_DEV_API_ENV;
});

it('uses the dev URL even when the remote feature flag points elsewhere', () => {
chompApiServiceInit(
getInitRequestMock({
remoteFeatureFlags: {
earnChompApiConfig: { baseUrl: 'https://chomp.example.com' },
},
}),
);

expect(jest.mocked(ChompApiService)).toHaveBeenCalledWith({
messenger: expect.any(Object),
baseUrl: 'https://chomp.dev-api.cx.metamask.io',
});
});
});
});
Loading
Loading