diff --git a/.github/workflows/build-android-upload-to-browserstack.yml b/.github/workflows/build-android-upload-to-browserstack.yml
index f843960b2d2..fae86af8478 100644
--- a/.github/workflows/build-android-upload-to-browserstack.yml
+++ b/.github/workflows/build-android-upload-to-browserstack.yml
@@ -257,7 +257,7 @@ jobs:
ENV_VARS=$(jq -n \
--arg byoa "${{ secrets.E2E_BYOA_AUTH_SECRET }}" \
--arg email "${{ secrets.E2E_MOCK_OAUTH_EMAIL }}" \
- '[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true}]')
+ '[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true},{"mapped_to":"OAUTH_BUILD_TYPE","value":"main_uat","is_expand":true}]')
CUSTOM_ID="MetaMask-Android-Without-SRP-${{ github.run_id }}"
# Trigger Android workflow
diff --git a/.github/workflows/build-ios-upload-to-browserstack.yml b/.github/workflows/build-ios-upload-to-browserstack.yml
index 580f1f38b32..dc11ecbe760 100644
--- a/.github/workflows/build-ios-upload-to-browserstack.yml
+++ b/.github/workflows/build-ios-upload-to-browserstack.yml
@@ -258,7 +258,7 @@ jobs:
ENV_VARS=$(jq -n \
--arg byoa "${{ secrets.E2E_BYOA_AUTH_SECRET }}" \
--arg email "${{ secrets.E2E_MOCK_OAUTH_EMAIL }}" \
- '[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true}]')
+ '[{"mapped_to":"DISABLE_NOTIFICATION_PROMPT","value":"true","is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH","value":"true","is_expand":true},{"mapped_to":"E2E_BYOA_AUTH_SECRET","value":$byoa,"is_expand":true},{"mapped_to":"E2E_MOCK_OAUTH_EMAIL","value":$email,"is_expand":true},{"mapped_to":"OAUTH_BUILD_TYPE","value":"main_uat","is_expand":true}]')
CUSTOM_ID="MetaMask-iOS-Without-SRP-${{ github.run_id }}"
# Trigger iOS workflow
diff --git a/android/app/src/main/res/xml/react_native_config.xml b/android/app/src/main/res/xml/react_native_config.xml
index aa2ca91bf79..6daea18d800 100644
--- a/android/app/src/main/res/xml/react_native_config.xml
+++ b/android/app/src/main/res/xml/react_native_config.xml
@@ -1,10 +1,15 @@
+
+
+
+
+
+
- sslip.io
+ sslip.io
localhost
10.0.2.2
10.0.3.2
-
diff --git a/app/core/OAuthService/OAuthLoginHandlers/config.ts b/app/core/OAuthService/OAuthLoginHandlers/config.ts
index 60e96839a6b..fec41aedd0c 100644
--- a/app/core/OAuthService/OAuthLoginHandlers/config.ts
+++ b/app/core/OAuthService/OAuthLoginHandlers/config.ts
@@ -10,7 +10,7 @@ interface OAUTH_CONFIG_TYPE {
IOS_APPLE_AUTH_CONNECTION_ID: string;
}
-enum BUILD_TYPE {
+export enum BUILD_TYPE {
development = 'development',
main_prod = 'main_prod',
main_uat = 'main_uat',
diff --git a/app/core/OAuthService/OAuthLoginHandlers/constants.ts b/app/core/OAuthService/OAuthLoginHandlers/constants.ts
index 6e34dbdd129..fa57008532e 100644
--- a/app/core/OAuthService/OAuthLoginHandlers/constants.ts
+++ b/app/core/OAuthService/OAuthLoginHandlers/constants.ts
@@ -1,10 +1,10 @@
import { ACTIONS, PREFIXES, PROTOCOLS } from '../../../constants/deeplinks';
import Device from '../../../util/device';
import ReduxService from '../../redux';
-import { isQa } from '../../../util/test/utils';
import AppConstants from '../../AppConstants';
import { AuthConnection } from '../OAuthInterface';
import { OAUTH_CONFIG } from './config';
+import { resolveOAuthConfigKey } from './oauthBuildType';
import {
DEFAULT_LEGACY_IOS_GOOGLE_CONFIG_ENABLED,
selectLegacyIosGoogleConfigEnabled,
@@ -13,45 +13,8 @@ import {
export const SEEDLESS_ONBOARDING_ENABLED =
process.env.SEEDLESS_ONBOARDING_ENABLED === 'true';
-/**
- * Mapping of old Build Type to new BuildType formatting for oauth config
- * Main -> main_prod
- * QA -> main_uat
- * Debug -> main_dev
- * flask -> flask_prod
- * flask QA -> flask_uat
- * flask Debug -> flask_dev
- *
- * new build types
- * main_beta -> main_prod
- * main_rc -> main_prod
- *
- * @param buildType - The build type to map
- * @param isDev - Whether the build is a development build
- * @returns The mapped build type
- */
-const buildTypeMapping = (buildType: string, isDev: boolean) => {
- // use development config for now
- if (process.env.DEV_OAUTH_CONFIG === 'true' && isDev) {
- return 'development';
- }
-
- switch (buildType) {
- case 'qa':
- return 'main_uat';
- case 'main':
- return isQa ? 'main_uat' : isDev ? 'main_dev' : 'main_prod';
- case 'flask':
- return isQa ? 'flask_uat' : isDev ? 'flask_dev' : 'flask_prod';
- default:
- return 'development';
- }
-};
-
-const BuildType = buildTypeMapping(
- AppConstants.METAMASK_BUILD_TYPE || 'main',
- AppConstants.IS_DEV,
-);
+/** OAuth config key: env override or build-type mapping */
+const BuildType = resolveOAuthConfigKey();
const CURRENT_OAUTH_CONFIG = OAUTH_CONFIG[BuildType];
export const web3AuthNetwork = CURRENT_OAUTH_CONFIG.WEB3AUTH_NETWORK;
diff --git a/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.test.ts b/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.test.ts
new file mode 100644
index 00000000000..d0e113dd47d
--- /dev/null
+++ b/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.test.ts
@@ -0,0 +1,83 @@
+import { BUILD_TYPE, OAUTH_CONFIG } from './config';
+import { buildTypeMapping } from './oauthBuildType';
+
+describe('buildTypeMapping', () => {
+ const originalDevOAuth = process.env.DEV_OAUTH_CONFIG;
+
+ afterEach(() => {
+ if (originalDevOAuth === undefined) {
+ delete process.env.DEV_OAUTH_CONFIG;
+ } else {
+ process.env.DEV_OAUTH_CONFIG = originalDevOAuth;
+ }
+ });
+
+ it('returns development when DEV_OAUTH_CONFIG is true and isDev', () => {
+ process.env.DEV_OAUTH_CONFIG = 'true';
+ expect(buildTypeMapping('main', true, false)).toBe(BUILD_TYPE.development);
+ });
+
+ it('maps qa to main_uat', () => {
+ expect(buildTypeMapping('qa', false, false)).toBe(BUILD_TYPE.main_uat);
+ });
+
+ it('maps main with QA channel to main_uat', () => {
+ expect(buildTypeMapping('main', false, true)).toBe(BUILD_TYPE.main_uat);
+ });
+
+ it('maps main without QA to main_dev when isDev', () => {
+ expect(buildTypeMapping('main', true, false)).toBe(BUILD_TYPE.main_dev);
+ });
+
+ it('maps main without QA to main_prod when not isDev', () => {
+ expect(buildTypeMapping('main', false, false)).toBe(BUILD_TYPE.main_prod);
+ });
+
+ it('maps flask with QA channel to flask_uat', () => {
+ expect(buildTypeMapping('flask', false, true)).toBe(BUILD_TYPE.flask_uat);
+ });
+
+ it('maps flask without QA to flask_dev when isDev', () => {
+ expect(buildTypeMapping('flask', true, false)).toBe(BUILD_TYPE.flask_dev);
+ });
+
+ it('maps flask without QA to flask_prod when not isDev', () => {
+ expect(buildTypeMapping('flask', false, false)).toBe(BUILD_TYPE.flask_prod);
+ });
+
+ it('returns development for unknown build type', () => {
+ expect(buildTypeMapping('unknown', false, false)).toBe(
+ BUILD_TYPE.development,
+ );
+ });
+});
+
+describe('resolveOAuthConfigKey', () => {
+ const originalOauthBuildType = process.env.OAUTH_BUILD_TYPE;
+
+ afterEach(() => {
+ if (originalOauthBuildType === undefined) {
+ delete process.env.OAUTH_BUILD_TYPE;
+ } else {
+ process.env.OAUTH_BUILD_TYPE = originalOauthBuildType;
+ }
+ });
+
+ it('returns OAUTH_BUILD_TYPE when set to a valid config key', () => {
+ jest.resetModules();
+ process.env.OAUTH_BUILD_TYPE = BUILD_TYPE.main_prod;
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires -- Jest reload after resetModules; dynamic import needs experimental-vm-modules
+ const { resolveOAuthConfigKey } = require('./oauthBuildType');
+ expect(resolveOAuthConfigKey()).toBe(BUILD_TYPE.main_prod);
+ });
+
+ it('ignores OAUTH_BUILD_TYPE when not a key of OAUTH_CONFIG', () => {
+ jest.resetModules();
+ process.env.OAUTH_BUILD_TYPE = 'not_a_real_key';
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+ const { resolveOAuthConfigKey } = require('./oauthBuildType');
+ const key = resolveOAuthConfigKey();
+ expect(key).not.toBe('not_a_real_key');
+ expect(key in OAUTH_CONFIG).toBe(true);
+ });
+});
diff --git a/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.ts b/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.ts
new file mode 100644
index 00000000000..9888697283a
--- /dev/null
+++ b/app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.ts
@@ -0,0 +1,56 @@
+import AppConstants from '../../AppConstants';
+import { isQa } from '../../../util/test/utils';
+import { BUILD_TYPE, OAUTH_CONFIG } from './config';
+
+/**
+ * Maps MetaMask build type + dev/QA flags to OAuth config keys.
+ * @param buildType - e.g. main, qa, flask
+ * @param isDev - development build
+ * @param isQaChannel - QA / e2e / exp channel
+ */
+export function buildTypeMapping(
+ buildType: string,
+ isDev: boolean,
+ isQaChannel: boolean,
+): BUILD_TYPE {
+ if (process.env.DEV_OAUTH_CONFIG === 'true' && isDev) {
+ return BUILD_TYPE.development;
+ }
+
+ switch (buildType) {
+ case 'qa':
+ return BUILD_TYPE.main_uat;
+ case 'main': {
+ if (isQaChannel) return BUILD_TYPE.main_uat;
+ if (isDev) return BUILD_TYPE.main_dev;
+ return BUILD_TYPE.main_prod;
+ }
+ case 'flask': {
+ if (isQaChannel) return BUILD_TYPE.flask_uat;
+ if (isDev) return BUILD_TYPE.flask_dev;
+ return BUILD_TYPE.flask_prod;
+ }
+ default:
+ return BUILD_TYPE.development;
+ }
+}
+
+/**
+ * Resolves which {@link OAUTH_CONFIG} entry applies (env override or build mapping).
+ */
+export function resolveOAuthConfigKey(): keyof typeof OAUTH_CONFIG {
+ const fromEnv = process.env.OAUTH_BUILD_TYPE;
+ if (
+ typeof fromEnv === 'string' &&
+ fromEnv.length > 0 &&
+ fromEnv in OAUTH_CONFIG
+ ) {
+ return fromEnv as keyof typeof OAUTH_CONFIG;
+ }
+
+ return buildTypeMapping(
+ AppConstants.METAMASK_BUILD_TYPE || 'main',
+ AppConstants.IS_DEV,
+ isQa,
+ );
+}
diff --git a/app/core/OAuthService/OAuthService.test.ts b/app/core/OAuthService/OAuthService.test.ts
index 18580d08698..4008fa22619 100644
--- a/app/core/OAuthService/OAuthService.test.ts
+++ b/app/core/OAuthService/OAuthService.test.ts
@@ -6,7 +6,7 @@ import { OAuthError, OAuthErrorType } from './error';
import { Web3AuthNetwork } from '@metamask/seedless-onboarding-controller';
import { TraceName, TraceOperation } from '../../util/trace';
import { signOut as acmSignOut } from '@metamask/react-native-acm';
-import { SET_SEEDLESS_ONBOARDING } from '../../actions/onboarding';
+const MOCK_GOOGLE_OAUTH_CLIENT_ID = 'abc.apps.googleusercontent.com';
const MOCK_JWT_TOKEN =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InN3bmFtOTA5QGdtYWlsLmNvbSIsInN1YiI6InN3bmFtOTA5QGdtYWlsLmNvbSIsImlzcyI6Im1ldGFtYXNrIiwiYXVkIjoibWV0YW1hc2siLCJpYXQiOjE3NDUyMDc1NjYsImVhdCI6MTc0NTIwNzg2NiwiZXhwIjoxNzQ1MjA3ODY2fQ.nXRRLB7fglRll7tMzFFCU0u7Pu6EddqEYf_DMyRgOENQ6tJ8OLtVknNf83_5a67kl_YKHFO-0PEjvJviPID6xg';
@@ -123,7 +123,7 @@ const mockGetAuthTokens = jest.fn().mockImplementation(() => ({
const mockCreateLoginHandler = jest.fn().mockImplementation(() => ({
authConnection: AuthConnection.Google,
options: {
- clientId: 'e2e-mock-google-client-id',
+ clientId: MOCK_GOOGLE_OAUTH_CLIENT_ID,
authServerUrl: 'https://auth.example.com',
web3AuthNetwork: 'sapphire_mainnet',
},
@@ -221,7 +221,7 @@ describe('OAuth login service', () => {
}),
);
expect(mockDispatch).toHaveBeenCalledWith({
- type: SET_SEEDLESS_ONBOARDING,
+ type: 'SET_SEEDLESS_ONBOARDING',
clientId: 'clientId',
authConnection: AuthConnection.Google,
});
@@ -675,7 +675,7 @@ describe('OAuth login service', () => {
delete process.env.E2E_MOCK_OAUTH_EMAIL;
});
- it('exchanges QA mock tokens and returns mock success without seedless authenticate', async () => {
+ it('exchanges QA mock tokens, calls seedless authenticate, and dispatches seedless onboarding', async () => {
const loginHandler = mockCreateLoginHandler();
const result = await OAuthLoginService.handleOAuthLogin(
@@ -700,15 +700,18 @@ describe('OAuth login service', () => {
const body = JSON.parse(
(fetchSpy.mock.calls[0][1] as RequestInit).body as string,
);
- expect(body).toMatchObject({
- email_id: 'newuser+e2e@web3auth.io',
- client_id: 'e2e-mock-google-client-id',
- login_provider: AuthConnection.Google,
- access_type: 'offline',
- });
- expect(mockAuthenticate).not.toHaveBeenCalled();
+ expect(body.client_id).toBe(MOCK_GOOGLE_OAUTH_CLIENT_ID);
+ expect(body.login_provider).toBe(AuthConnection.Google);
+ expect(body.access_type).toBe('offline');
+ expect(body.email_id).toMatch(/^[a-f0-9]+\d+\+e2e@web3auth\.io$/);
+ expect(mockAuthenticate).toHaveBeenCalledTimes(1);
expect(mockLoginHandlerResponse).not.toHaveBeenCalled();
expect(mockGetAuthTokens).not.toHaveBeenCalled();
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: 'SET_SEEDLESS_ONBOARDING',
+ clientId: MOCK_GOOGLE_OAUTH_CLIENT_ID,
+ authConnection: AuthConnection.Google,
+ });
});
it('uses E2E_MOCK_OAUTH_EMAIL for email_id when set', async () => {
@@ -739,7 +742,7 @@ describe('OAuth login service', () => {
expect(mockAuthenticate).not.toHaveBeenCalled();
});
- it('succeeds when QA mock response omits refresh_token', async () => {
+ it('rejects when QA mock response omits refresh_token (seedless authenticate requires it)', async () => {
fetchSpy.mockResolvedValueOnce({
ok: true,
status: 200,
@@ -757,23 +760,27 @@ describe('OAuth login service', () => {
} as Response);
const loginHandler = mockCreateLoginHandler();
- const result = await OAuthLoginService.handleOAuthLogin(
- loginHandler,
- false,
+ await expectOAuthError(
+ OAuthLoginService.handleOAuthLogin(loginHandler, false),
+ OAuthErrorType.LoginError,
);
- expect(result.type).toBe('success');
expect(mockAuthenticate).not.toHaveBeenCalled();
});
- it('does not call provider login, getAuthTokens, or seedless authenticate', async () => {
+ it('does not call provider login or getAuthTokens but does call seedless authenticate', async () => {
const loginHandler = mockCreateLoginHandler();
await OAuthLoginService.handleOAuthLogin(loginHandler, false);
expect(mockLoginHandlerResponse).not.toHaveBeenCalled();
expect(mockGetAuthTokens).not.toHaveBeenCalled();
- expect(mockAuthenticate).not.toHaveBeenCalled();
+ expect(mockAuthenticate).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith({
+ type: 'SET_SEEDLESS_ONBOARDING',
+ clientId: MOCK_GOOGLE_OAUTH_CLIENT_ID,
+ authConnection: AuthConnection.Google,
+ });
});
});
});
diff --git a/app/core/OAuthService/OAuthService.ts b/app/core/OAuthService/OAuthService.ts
index 32bf4500b09..2debff7f404 100644
--- a/app/core/OAuthService/OAuthService.ts
+++ b/app/core/OAuthService/OAuthService.ts
@@ -140,10 +140,6 @@ export class OAuthService {
throw new Error('No user id found');
}
- if (isE2EMockOAuth()) {
- return QAMockOAuthService.mockSeedlessHandleResult(accountName);
- }
-
const authConnectionConfig = getAuthConnectionIdFromClientId({
clientId,
authConnection,
@@ -214,6 +210,14 @@ export class OAuthService {
);
this.#dispatchPostLogin(result);
+
+ ReduxService.store.dispatch(
+ setSeedlessOnboarding({
+ clientId: loginHandler.options.clientId,
+ authConnection: loginHandler.authConnection,
+ }),
+ );
+
return result;
};
diff --git a/app/core/OAuthService/QAMockOAuthService.test.ts b/app/core/OAuthService/QAMockOAuthService.test.ts
index 7e108af1aea..d78be1bf023 100644
--- a/app/core/OAuthService/QAMockOAuthService.test.ts
+++ b/app/core/OAuthService/QAMockOAuthService.test.ts
@@ -5,8 +5,18 @@ import type { BaseLoginHandler } from './OAuthLoginHandlers/baseHandler';
const MOCK_JWT_TOKEN =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InN3bmFtOTA5QGdtYWlsLmNvbSIsInN1YiI6InN3bmFtOTA5QGdtYWlsLmNvbSIsImlzcyI6Im1ldGFtYXNrIiwiYXVkIjoibWV0YW1hc2siLCJpYXQiOjE3NDUyMDc1NjYsImVhdCI6MTc0NTIwNzg2NiwiZXhwIjoxNzQ1MjA3ODY2fQ.nXRRLB7fglRll7tMzFFCU0u7Pu6EddqEYf_DMyRgOENQ6tJ8OLtVknNf83_5a67kl_YKHFO-0PEjvJviPID6xg';
+jest.mock('./OAuthLoginHandlers/constants', () => {
+ const actual = jest.requireActual<
+ typeof import('./OAuthLoginHandlers/constants')
+ >('./OAuthLoginHandlers/constants');
+ return {
+ ...actual,
+ E2E_QA_MOCK_OAUTH_TOKEN_URL: 'https://test.qa.mock/oauth/token',
+ };
+});
+
const mockGetE2EByoaAuthSecret = jest.fn(
- () => 'test-byoa-secret',
+ () => undefined,
);
const mockGetE2EMockOAuthEmailForQaMock = jest.fn(
() => undefined,
@@ -18,23 +28,27 @@ jest.mock('../../util/environment', () => ({
getE2EMockOAuthEmailForQaMock: () => mockGetE2EMockOAuthEmailForQaMock(),
}));
-jest.mock('./OAuthLoginHandlers/constants', () => {
- const actual = jest.requireActual<
- typeof import('./OAuthLoginHandlers/constants')
- >('./OAuthLoginHandlers/constants');
- return {
- ...actual,
- E2E_QA_MOCK_OAUTH_TOKEN_URL: 'https://test.qa.mock/oauth/token',
- };
-});
+jest.mock('react-native-quick-crypto', () => ({
+ __esModule: true,
+ default: {
+ randomBytes: jest.fn((size: number) =>
+ Buffer.from(Array.from({ length: size }, (_, i) => (i % 255) + 1)),
+ ),
+ },
+}));
import { QAMockOAuthService } from './QAMockOAuthService';
+const MOCK_GOOGLE_OAUTH_CLIENT_ID =
+ process.env.IOS_GOOGLE_CLIENT_ID ||
+ process.env.ANDROID_GOOGLE_SERVER_CLIENT_ID ||
+ 'abc.apps.googleusercontent.com';
+
const createStubLoginHandler = (): BaseLoginHandler =>
({
authConnection: AuthConnection.Google,
options: {
- clientId: 'e2e-mock-google-client-id',
+ clientId: MOCK_GOOGLE_OAUTH_CLIENT_ID,
authServerUrl: 'https://auth.example.com',
web3AuthNetwork: 'sapphire_mainnet',
},
@@ -53,7 +67,7 @@ const createStubLoginHandler = (): BaseLoginHandler =>
describe('QAMockOAuthService', () => {
beforeEach(() => {
jest.clearAllMocks();
- mockGetE2EByoaAuthSecret.mockReturnValue('test-byoa-secret');
+ mockGetE2EByoaAuthSecret.mockReturnValue(undefined);
mockGetE2EMockOAuthEmailForQaMock.mockReturnValue(undefined);
});
@@ -101,20 +115,8 @@ describe('QAMockOAuthService', () => {
});
});
- describe('mockSeedlessHandleResult', () => {
- it('returns success with existingUser false and accountName', () => {
- const result =
- QAMockOAuthService.mockSeedlessHandleResult('user@example.com');
-
- expect(result.type).toBe(OAuthLoginResultType.SUCCESS);
- expect(result.existingUser).toBe(false);
- expect(result.accountName).toBe('user@example.com');
- });
- });
-
describe('exchangeTokens', () => {
it('uses default BYOA secret when env secret is unset', async () => {
- mockGetE2EByoaAuthSecret.mockReturnValue(undefined);
const envelope = {
success: true,
data: {
@@ -148,6 +150,10 @@ describe('QAMockOAuthService', () => {
});
it('POSTs QA mock URL and returns data userId and accountName', async () => {
+ mockGetE2EByoaAuthSecret.mockReturnValue('test-byoa-secret');
+ mockGetE2EMockOAuthEmailForQaMock.mockReturnValue(
+ 'newuser+e2e@web3auth.io',
+ );
const envelope = {
success: true,
data: {
@@ -185,7 +191,7 @@ describe('QAMockOAuthService', () => {
);
expect(body).toMatchObject({
email_id: 'newuser+e2e@web3auth.io',
- client_id: 'e2e-mock-google-client-id',
+ client_id: MOCK_GOOGLE_OAUTH_CLIENT_ID,
login_provider: AuthConnection.Google,
access_type: 'offline',
});
diff --git a/app/core/OAuthService/QAMockOAuthService.ts b/app/core/OAuthService/QAMockOAuthService.ts
index c8147aa7cf5..735bd74d780 100644
--- a/app/core/OAuthService/QAMockOAuthService.ts
+++ b/app/core/OAuthService/QAMockOAuthService.ts
@@ -1,16 +1,12 @@
-import {
- getE2EByoaAuthSecret,
- getE2EMockOAuthEmailForQaMock,
-} from '../../util/environment';
import { E2E_QA_MOCK_OAUTH_TOKEN_URL } from './OAuthLoginHandlers/constants';
import { OAuthError, OAuthErrorType } from './error';
import { BaseLoginHandler } from './OAuthLoginHandlers/baseHandler';
+import { type AuthResponse, type OAuthUserInfo } from './OAuthInterface';
import {
- OAuthLoginResultType,
- type AuthResponse,
- type HandleOAuthLoginResult,
- type OAuthUserInfo,
-} from './OAuthInterface';
+ getE2EByoaAuthSecret,
+ getE2EMockOAuthEmailForQaMock,
+} from '../../util/environment';
+import QuickCrypto from 'react-native-quick-crypto';
export interface QAMockTokenExchangeResult {
data: AuthResponse;
@@ -20,6 +16,11 @@ export interface QAMockTokenExchangeResult {
const DEFAULT_E2E_BYOA_AUTH_SECRET = '6SMBaAx6*TG8AEQ+7Ap#zEUAIZ42';
+const generateUniqueE2EEmail = (): string => {
+ const rand = QuickCrypto.randomBytes(4).toString('hex').slice(0, 8);
+ return `${rand}${Date.now()}+e2e@web3auth.io`;
+};
+
export class QAMockOAuthService {
static async exchangeTokens(
loginHandler: BaseLoginHandler,
@@ -27,9 +28,8 @@ export class QAMockOAuthService {
): Promise {
const byoaSecret = getE2EByoaAuthSecret() ?? DEFAULT_E2E_BYOA_AUTH_SECRET;
- const e2eEmail = `${loginHandler.authConnection}.newuser+e2e@web3auth.io`;
const emailForMock =
- getE2EMockOAuthEmailForQaMock() ?? 'newuser+e2e@web3auth.io';
+ getE2EMockOAuthEmailForQaMock() ?? generateUniqueE2EEmail();
const response = await fetchImpl(E2E_QA_MOCK_OAUTH_TOKEN_URL, {
method: 'POST',
@@ -58,22 +58,12 @@ export class QAMockOAuthService {
const jwtPayload = JSON.parse(
loginHandler.decodeIdToken(data.id_token),
) as Partial;
- const userId = jwtPayload.sub ?? `e2e-user-${e2eEmail}`;
- const accountName = jwtPayload.email ?? e2eEmail;
+ const userId = jwtPayload.sub ?? `e2e-user-${emailForMock}`;
+ const accountName = jwtPayload.email ?? emailForMock;
return { data, userId, accountName };
}
- static mockSeedlessHandleResult(
- accountName?: string,
- ): HandleOAuthLoginResult {
- return {
- type: OAuthLoginResultType.SUCCESS,
- existingUser: false,
- accountName,
- };
- }
-
static parseAuthServiceResponse(raw: unknown): AuthResponse {
if (raw === null || typeof raw !== 'object') {
throw new OAuthError(
diff --git a/app/util/environment.ts b/app/util/environment.ts
index 11c1df95755..d95f6953288 100644
--- a/app/util/environment.ts
+++ b/app/util/environment.ts
@@ -9,12 +9,12 @@ export const isProduction = (): boolean =>
export const isE2EMockOAuth = (): boolean =>
process.env.E2E_MOCK_OAUTH === 'true';
-export const getE2EByoaAuthSecret = (): string | undefined => {
+export function getE2EByoaAuthSecret(): string | undefined {
const secret = process.env.E2E_BYOA_AUTH_SECRET;
return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
-};
+}
-export const getE2EMockOAuthEmailForQaMock = (): string | undefined => {
+export function getE2EMockOAuthEmailForQaMock(): string | undefined {
const email = process.env.E2E_MOCK_OAUTH_EMAIL;
return typeof email === 'string' && email.length > 0 ? email : undefined;
-};
+}
diff --git a/app/util/test/testSetup.js b/app/util/test/testSetup.js
index 06ca789fb08..efb2e73814a 100644
--- a/app/util/test/testSetup.js
+++ b/app/util/test/testSetup.js
@@ -23,52 +23,58 @@ jest.mock('expo/fetch', () => {
};
});
-jest.mock('react-native-quick-crypto', () => ({
- getRandomValues: jest.fn((array) => {
- for (let i = 0; i < array.length; i++) {
- array[i] = Math.floor(Math.random() * 256);
- }
- return array;
- }),
- subtle: {
- importKey: jest.fn((format, keyData, algorithm, extractable, keyUsages) => {
- return Promise.resolve({
- format,
- keyData,
- algorithm,
- extractable,
- keyUsages,
- });
- }),
- deriveBits: jest.fn((algorithm, baseKey, length) => {
- const derivedBits = new Uint8Array(length);
- for (let i = 0; i < length; i++) {
- derivedBits[i] = Math.floor(Math.random() * 256);
+jest.mock('react-native-quick-crypto', () => {
+ const { randomBytes: nodeRandomBytes } = require('crypto');
+ return {
+ getRandomValues: jest.fn((array) => {
+ for (let i = 0; i < array.length; i++) {
+ array[i] = Math.floor(Math.random() * 256);
}
- return Promise.resolve(derivedBits);
- }),
- exportKey: jest.fn((format, key) => {
- return Promise.resolve(new Uint8Array([1, 2, 3, 4]));
+ return array;
}),
- encrypt: jest.fn((algorithm, key, data) => {
- return Promise.resolve(
- new Uint8Array([
- 123, 34, 116, 101, 115, 116, 34, 58, 34, 100, 97, 116, 97, 34, 125,
- ]),
- );
- }),
- decrypt: jest.fn((algorithm, key, data) => {
- return Promise.resolve(
- new Uint8Array([
- 123, 34, 116, 101, 115, 116, 34, 58, 34, 100, 97, 116, 97, 34, 125,
- ]),
- );
- }),
- },
- randomUUID: jest.fn(
- () => 'mock-uuid-' + Math.random().toString(36).slice(2, 11),
- ),
-}));
+ randomBytes: jest.fn((size) => nodeRandomBytes(size)),
+ subtle: {
+ importKey: jest.fn(
+ (format, keyData, algorithm, extractable, keyUsages) => {
+ return Promise.resolve({
+ format,
+ keyData,
+ algorithm,
+ extractable,
+ keyUsages,
+ });
+ },
+ ),
+ deriveBits: jest.fn((algorithm, baseKey, length) => {
+ const derivedBits = new Uint8Array(length);
+ for (let i = 0; i < length; i++) {
+ derivedBits[i] = Math.floor(Math.random() * 256);
+ }
+ return Promise.resolve(derivedBits);
+ }),
+ exportKey: jest.fn((format, key) => {
+ return Promise.resolve(new Uint8Array([1, 2, 3, 4]));
+ }),
+ encrypt: jest.fn((algorithm, key, data) => {
+ return Promise.resolve(
+ new Uint8Array([
+ 123, 34, 116, 101, 115, 116, 34, 58, 34, 100, 97, 116, 97, 34, 125,
+ ]),
+ );
+ }),
+ decrypt: jest.fn((algorithm, key, data) => {
+ return Promise.resolve(
+ new Uint8Array([
+ 123, 34, 116, 101, 115, 116, 34, 58, 34, 100, 97, 116, 97, 34, 125,
+ ]),
+ );
+ }),
+ },
+ randomUUID: jest.fn(
+ () => 'mock-uuid-' + Math.random().toString(36).slice(2, 11),
+ ),
+ };
+});
// Create a persistent mock function that survives Jest teardown
const mockBatchedUpdates = jest.fn((fn) => {
diff --git a/babel.config.tests.js b/babel.config.tests.js
index 0a2706e97d4..671e1da6a3d 100644
--- a/babel.config.tests.js
+++ b/babel.config.tests.js
@@ -55,6 +55,8 @@ const newOverrides = [
'app/components/UI/Card/util/mapBaanxApiUrl.test.ts',
'app/core/Engine/controllers/card-controller/services/baanx-config.ts',
'app/core/Engine/controllers/card-controller/services/baanx-config.test.ts',
+ 'app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.ts',
+ 'app/core/OAuthService/OAuthLoginHandlers/oauthBuildType.test.ts',
'app/components/UI/Predict/providers/polymarket/protocol/definitions.ts',
'app/components/UI/Predict/providers/polymarket/protocol/definitions.test.ts',
'app/store/migrations/**',
diff --git a/metro.config.js b/metro.config.js
index 2cdcde3aa83..b5ece123f24 100644
--- a/metro.config.js
+++ b/metro.config.js
@@ -171,7 +171,10 @@ module.exports = function (baseConfig) {
};
}
}
- if (e2eAllowsSeedlessOAuthMetroMocks) {
+ // Mock the seedless-onboarding-controller only for E2E smoke tests.
+ // Performance tests (E2E_MOCK_OAUTH only) use the real controller
+ // so TOPRF authentication runs against live nodes.
+ if (isE2E) {
if (
moduleName.endsWith(
'controllers/seedless-onboarding-controller',
@@ -190,6 +193,8 @@ module.exports = function (baseConfig) {
),
};
}
+ }
+ if (e2eAllowsSeedlessOAuthMetroMocks) {
// Skips native Google/Apple UI; tokens still hit auth server (see module mock).
if (
moduleName.endsWith('OAuthService/OAuthLoginHandlers') ||
diff --git a/tests/api-mocking/seedless-onboarding/OAuthMockttpService.ts b/tests/api-mocking/seedless-onboarding/OAuthMockttpService.ts
index e2d8c589b38..3a1508df45b 100644
--- a/tests/api-mocking/seedless-onboarding/OAuthMockttpService.ts
+++ b/tests/api-mocking/seedless-onboarding/OAuthMockttpService.ts
@@ -18,6 +18,25 @@ import {
import { OAuthMockttpServiceOptions, SecretType } from './types';
+const DEFAULT_MOCK_APPLE_OAUTH_CLIENT_ID = 'io.metamask.appleloginclient.dev';
+const DEFAULT_MOCK_GOOGLE_OAUTH_CLIENT_ID =
+ '615965109465-i8oeh9kuvl1n6lk1ffkobpvth27bmi41.apps.googleusercontent.com';
+
+function defaultMockOAuthClientIdForTokenRequest(
+ loginProvider: E2ELoginProvider,
+): string {
+ if (loginProvider === E2ELoginProvider.APPLE) {
+ return (
+ process.env.ANDROID_APPLE_CLIENT_ID || DEFAULT_MOCK_APPLE_OAUTH_CLIENT_ID
+ );
+ }
+ return (
+ process.env.IOS_GOOGLE_CLIENT_ID ||
+ process.env.ANDROID_GOOGLE_SERVER_CLIENT_ID ||
+ DEFAULT_MOCK_GOOGLE_OAUTH_CLIENT_ID
+ );
+}
+
/**
* Configuration for E2E OAuth mock
*/
@@ -265,7 +284,11 @@ export class OAuthMockttpService {
const mockRequestBody = {
email_id: emailForMock,
- client_id: body.client_id || 'e2e-mock-client-id',
+ client_id:
+ body.client_id ||
+ defaultMockOAuthClientIdForTokenRequest(
+ this.config.loginProvider,
+ ),
login_provider: this.config.loginProvider,
access_type: 'offline',
};
@@ -289,12 +312,17 @@ export class OAuthMockttpService {
let tokens;
if (!response.ok) {
+ const errorText = await response.text().catch(() => '');
console.warn(
- `[E2E] Backend QA mock returned ${response.status}, using fallback mock tokens`,
+ `[E2E] Backend QA mock returned ${response.status}: ${errorText}, using fallback mock tokens`,
);
tokens = this.generateMockAuthResponse();
} else {
const rawResponse = await response.json();
+ console.log(
+ '[E2E] QA mock raw response:',
+ JSON.stringify(rawResponse, null, 2),
+ );
const fallbackTokens = this.generateMockAuthResponse();
if (rawResponse.data?.tokens) {
const qaMockTokens = rawResponse.data.tokens;
@@ -361,7 +389,11 @@ export class OAuthMockttpService {
const mockRequestBody = {
email_id: emailForMock,
- client_id: body.client_id || 'e2e-mock-client-id',
+ client_id:
+ body.client_id ||
+ defaultMockOAuthClientIdForTokenRequest(
+ this.config.loginProvider,
+ ),
login_provider: this.config.loginProvider,
};
@@ -433,7 +465,11 @@ export class OAuthMockttpService {
const mockRequestBody = {
email_id: emailForMock,
- client_id: body.client_id || 'e2e-mock-client-id',
+ client_id:
+ body.client_id ||
+ defaultMockOAuthClientIdForTokenRequest(
+ this.config.loginProvider,
+ ),
login_provider: this.config.loginProvider,
refresh_token: body.refresh_token,
};
@@ -624,7 +660,15 @@ export class OAuthMockttpService {
console.log(
`[E2E MockServer] SSS Node ${nodeIndex} request: ${body.method}`,
);
+ console.log(
+ `[E2E MockServer] SSS Node ${nodeIndex} request body:`,
+ JSON.stringify(body, null, 2),
+ );
const response = this.handleToprfRequest(body, nodeIndex);
+ console.log(
+ `[E2E MockServer] SSS Node ${nodeIndex} response:`,
+ JSON.stringify(response, null, 2),
+ );
return {
statusCode: 200,
@@ -850,7 +894,7 @@ export class OAuthMockttpService {
const now = Math.floor(Date.now() / 1000);
const exp = now + 3600; // 1 hour expiry
- // Mock ID token payload
+ // Mock ID token payload (client_id prefers env OAuth client ids, same as QA proxy body)
const idTokenPayload = {
iss: 'https://auth-service.uat-api.cx.metamask.io',
sub: `e2e-user-${this.config.email}`,
@@ -859,6 +903,10 @@ export class OAuthMockttpService {
email_verified: true,
iat: now,
exp,
+ client_id: defaultMockOAuthClientIdForTokenRequest(
+ this.config.loginProvider,
+ ),
+ env: 'uat',
verifier: this.config.loginProvider,
verifier_id: this.config.email,
// Include aggregateVerifier for TOPRF
diff --git a/tests/module-mocking/oauth/OAuthLoginHandlers/index.ts b/tests/module-mocking/oauth/OAuthLoginHandlers/index.ts
index 4e2a6eddca0..bd26e7c3fd8 100644
--- a/tests/module-mocking/oauth/OAuthLoginHandlers/index.ts
+++ b/tests/module-mocking/oauth/OAuthLoginHandlers/index.ts
@@ -15,6 +15,25 @@ import {
} from '../../../../app/core/OAuthService/OAuthLoginHandlers/constants';
import type { BaseHandlerOptions } from '../../../../app/core/OAuthService/OAuthLoginHandlers/baseHandler';
+const MOCK_GOOGLE_OAUTH_CLIENT_ID_IOS =
+ process.env.MAIN_IOS_GOOGLE_CLIENT_ID_UAT;
+const MOCK_GOOGLE_OAUTH_CLIENT_ID_ANDROID =
+ process.env.MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT;
+
+function getMockGoogleOAuthClientId(): string {
+ const clientId =
+ Platform.OS === 'ios'
+ ? MOCK_GOOGLE_OAUTH_CLIENT_ID_IOS
+ : MOCK_GOOGLE_OAUTH_CLIENT_ID_ANDROID;
+ if (!clientId) {
+ throw new Error(
+ `[E2E Mock] Missing Google OAuth UAT client ID env var for platform "${Platform.OS}". ` +
+ 'Ensure MAIN_IOS_GOOGLE_CLIENT_ID_UAT or MAIN_ANDROID_GOOGLE_CLIENT_ID_UAT is set.',
+ );
+ }
+ return clientId;
+}
+
/**
* Login result type
*/
@@ -247,13 +266,19 @@ export function createLoginHandler(
switch (provider) {
case 'google':
return new MockGoogleLoginHandler({
- clientId: 'e2e-mock-google-client-id',
+ clientId: getMockGoogleOAuthClientId(),
redirectUri: 'metamask://e2e',
});
- case 'apple':
- return new MockAppleLoginHandler({
- clientId: 'e2e-mock-apple-client-id',
- });
+ case 'apple': {
+ const appleClientId = process.env.MAIN_ANDROID_APPLE_CLIENT_ID_UAT;
+ if (!appleClientId) {
+ throw new Error(
+ '[E2E Mock] Missing Apple OAuth UAT client ID. ' +
+ 'Ensure MAIN_ANDROID_APPLE_CLIENT_ID_UAT is set.',
+ );
+ }
+ return new MockAppleLoginHandler({ clientId: appleClientId });
+ }
default:
throw new Error(`[E2E Mock] Unsupported provider: ${provider}`);
}
diff --git a/tests/performance/onboarding/seedless-apple-onboarding.spec.ts b/tests/performance/onboarding/seedless-apple-onboarding.spec.ts
index 762e5dcd47f..208d2342ba5 100644
--- a/tests/performance/onboarding/seedless-apple-onboarding.spec.ts
+++ b/tests/performance/onboarding/seedless-apple-onboarding.spec.ts
@@ -10,7 +10,6 @@ import SocialLoginView from '../../page-objects/Onboarding/SocialLoginView';
import CreatePasswordView from '../../page-objects/Onboarding/CreatePasswordView';
import OnboardingSuccessView from '../../page-objects/Onboarding/OnboardingSuccessView';
import PredictModalView from '../../page-objects/Predict/PredictModalView';
-import WalletView from '../../page-objects/wallet/WalletView';
import LoginView from '../../page-objects/wallet/LoginView';
const waitForFirstSuccessful = async (promises: Promise[]): Promise =>
@@ -60,11 +59,6 @@ test.describe(PerformanceOnboarding, () => {
{ ios: 2500, android: 3100 },
currentDeviceDetails.platform,
);
- const timer6 = new TimerHelper(
- 'Apple: Dismiss feature sheet → wallet main screen visible',
- { ios: 30000, android: 30000 },
- currentDeviceDetails.platform,
- );
const password = getPasswordForScenario('onboarding') ?? '';
@@ -138,16 +132,8 @@ test.describe(PerformanceOnboarding, () => {
});
await dismisspredictionsModalPlaywright();
- await timer6.measure(async () => {
- await PlaywrightAssertions.expectElementToBeVisible(
- asPlaywrightElement(WalletView.container),
- {
- description: 'Wallet main screen should be visible',
- },
- );
- });
- const timers = [timer1, timer2, timer4, timer5, timer6];
+ const timers = [timer1, timer2, timer4, timer5];
if (currentDeviceDetails.platform === 'ios') {
timers.splice(2, 0, timer3);
}
@@ -161,16 +147,7 @@ test.describe(PerformanceOnboarding, () => {
await LoginView.enterPassword(password);
await LoginView.tapLoginButton();
- await timer4.measure(async () => {
- await PlaywrightAssertions.expectElementToBeVisible(
- asPlaywrightElement(WalletView.container),
- {
- description: 'Wallet main screen should be visible',
- },
- );
- });
-
- performanceTracker.addTimers(timer1, timer2, timer3, timer4);
+ performanceTracker.addTimers(timer1, timer2, timer3);
}
},
);
diff --git a/tests/performance/onboarding/seedless-google-onboarding.spec.ts b/tests/performance/onboarding/seedless-google-onboarding.spec.ts
index 809471d5faf..b6f02809fc5 100644
--- a/tests/performance/onboarding/seedless-google-onboarding.spec.ts
+++ b/tests/performance/onboarding/seedless-google-onboarding.spec.ts
@@ -10,7 +10,6 @@ import SocialLoginView from '../../page-objects/Onboarding/SocialLoginView';
import CreatePasswordView from '../../page-objects/Onboarding/CreatePasswordView';
import OnboardingSuccessView from '../../page-objects/Onboarding/OnboardingSuccessView';
import PredictModalView from '../../page-objects/Predict/PredictModalView';
-import WalletView from '../../page-objects/wallet/WalletView';
import LoginView from '../../page-objects/wallet/LoginView';
const waitForFirstSuccessful = async (promises: Promise[]): Promise =>
@@ -60,11 +59,6 @@ test.describe(PerformanceOnboarding, () => {
{ ios: 2500, android: 3100 },
currentDeviceDetails.platform,
);
- const timer6 = new TimerHelper(
- 'Google: Dismiss feature sheet → wallet main screen visible',
- { ios: 30000, android: 30000 },
- currentDeviceDetails.platform,
- );
const password = getPasswordForScenario('onboarding') ?? '';
@@ -138,16 +132,8 @@ test.describe(PerformanceOnboarding, () => {
});
await dismisspredictionsModalPlaywright();
- await timer6.measure(async () => {
- await PlaywrightAssertions.expectElementToBeVisible(
- asPlaywrightElement(WalletView.container),
- {
- description: 'Wallet main screen should be visible',
- },
- );
- });
- const timers = [timer1, timer2, timer4, timer5, timer6];
+ const timers = [timer1, timer2, timer4, timer5];
if (currentDeviceDetails.platform === 'ios') {
timers.splice(2, 0, timer3);
}
@@ -161,16 +147,7 @@ test.describe(PerformanceOnboarding, () => {
await LoginView.enterPassword(password);
await LoginView.tapLoginButton();
- await timer4.measure(async () => {
- await PlaywrightAssertions.expectElementToBeVisible(
- asPlaywrightElement(WalletView.container),
- {
- description: 'Wallet main screen should be visible',
- },
- );
- });
-
- performanceTracker.addTimers(timer1, timer2, timer3, timer4);
+ performanceTracker.addTimers(timer1, timer2, timer3);
}
},
);