Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
247c4fc
feat: seedless-onboarding controller init
lwin-kyaw May 11, 2025
b6ccfb9
feat: added Social login to UI
lwin-kyaw May 11, 2025
a7e5809
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 11, 2025
144e213
feat: updated selectors and ui
lwin-kyaw May 11, 2025
5e6f871
fix: fixed test and lint
lwin-kyaw May 12, 2025
198a6fd
refactor: renamed 'seedless' property to 'social' in 'FirstTimeFlow' …
lwin-kyaw May 12, 2025
07545cb
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 13, 2025
a91cbaa
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 21, 2025
a3f63e5
fix: fixed lint and unit tests
lwin-kyaw May 21, 2025
621c9ec
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 23, 2025
337f31c
chore: renamed 'provider' to 'authConnection' in action.startOAuthLogin
lwin-kyaw May 23, 2025
672d656
fix: use env instead of hardcoding in SeedlessControllerInit
lwin-kyaw May 23, 2025
0f983b7
fix: split FirstTimeFlow.social into FirstTimeFlow.socialCreate and F…
lwin-kyaw May 23, 2025
b069776
refactor: splited solicalLoginClick into two functoins, 'create' and …
lwin-kyaw May 23, 2025
c00bcb1
fix: hide 'loginOptionsModal' on click social login
lwin-kyaw May 23, 2025
696f323
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 23, 2025
4552713
feat: update firstTimeFlowType in account-exist/not-found pages
lwin-kyaw May 23, 2025
4e0a4b5
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 23, 2025
b52c3fc
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 23, 2025
1c2cce5
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 26, 2025
378ac8f
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 26, 2025
efefd96
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 26, 2025
21223f6
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 26, 2025
645a563
Merge branch 'feat/oauth-controller' into feat/social-login-sync
lionellbriones May 26, 2025
2c4a4cb
chore: remove mmi build flag
lionellbriones May 27, 2025
13592ff
chore: fix incorrect merge css
lionellbriones May 27, 2025
59a7ece
Merge remote-tracking branch 'origin/feat/oauth-controller' into feat…
lwin-kyaw May 28, 2025
c902283
Merge branch 'feat/oauth-controller' into feat/social-login-sync
lionellbriones May 28, 2025
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 app/scripts/controller-init/controller-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { Controller as NotificationServicesPushController } from '@metamask/noti
import { DelegationController } from '@metamask/delegation-controller';

import { RemoteFeatureFlagController } from '@metamask/remote-feature-flag-controller';
import { SeedlessOnboardingController } from '@metamask/seedless-onboarding-controller';
import OnboardingController from '../controllers/onboarding';
import { PreferencesController } from '../controllers/preferences-controller';
import SwapsController from '../controllers/swaps';
Expand Down Expand Up @@ -74,6 +75,7 @@ export type Controller =
| PPOMController
| PreferencesController
| RateLimitController<RateLimitedApiMap>
| SeedlessOnboardingController
| SmartTransactionsController
| SnapController
| SnapInterfaceController
Expand Down Expand Up @@ -111,6 +113,7 @@ export type ControllerFlatState = AccountsController['state'] &
>['state'] &
PPOMController['state'] &
PreferencesController['state'] &
SeedlessOnboardingController['state'] &
SmartTransactionsController['state'] &
SnapController['state'] &
SnapInsightsController['state'] &
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/controller-init/messengers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
getDelegationControllerInitMessenger,
getDelegationControllerMessenger,
} from './delegation/delegation-controller-messenger';
import { getSeedlessOnboardingControllerMessenger } from './seedless-onboarding';

export const CONTROLLER_MESSENGERS = {
AuthenticationController: {
Expand Down Expand Up @@ -104,6 +105,10 @@ export const CONTROLLER_MESSENGERS = {
getMessenger: getRateLimitControllerMessenger,
getInitMessenger: getRateLimitControllerInitMessenger,
},
SeedlessOnboardingController: {
getMessenger: getSeedlessOnboardingControllerMessenger,
getInitMessenger: noop,
},
SnapsRegistry: {
getMessenger: getSnapsRegistryMessenger,
getInitMessenger: noop,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { getSeedlessOnboardingControllerMessenger } from './seedless-onboarding-controller-messenger';
export type { SeedlessOnboardingControllerMessenger } from './seedless-onboarding-controller-messenger';
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Messenger, RestrictedMessenger } from '@metamask/base-controller';

import { getSeedlessOnboardingControllerMessenger } from './seedless-onboarding-controller-messenger';

describe('getSeedlessOnboardingControllerMessenger', () => {
it('returns a restricted messenger', () => {
const messenger = new Messenger<never, never>();
const seedlessOnboardingControllerMessenger =
getSeedlessOnboardingControllerMessenger(messenger);

expect(seedlessOnboardingControllerMessenger).toBeInstanceOf(
RestrictedMessenger,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Messenger } from '@metamask/base-controller';
import {
SeedlessOnboardingControllerGetStateAction,
SeedlessOnboardingControllerStateChangeEvent,
} from '@metamask/seedless-onboarding-controller';

export type SeedlessOnboardingControllerMessenger = ReturnType<
typeof getSeedlessOnboardingControllerMessenger
>;

/**
* Get a restricted messenger for the Seedless Onboarding controller. This is scoped to the
* actions and events that the Seedless Onboarding controller is allowed to handle.
*
* @param messenger - The messenger to restrict.
* @returns The restricted messenger.
*/
export function getSeedlessOnboardingControllerMessenger(
messenger: Messenger<
SeedlessOnboardingControllerGetStateAction,
SeedlessOnboardingControllerStateChangeEvent
>,
) {
return messenger.getRestricted({
name: 'SeedlessOnboardingController',
allowedActions: [],
allowedEvents: [],
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
SeedlessOnboardingController,
Web3AuthNetwork,
} from '@metamask/seedless-onboarding-controller';
import { Messenger } from '@metamask/base-controller';
import { ControllerInitRequest } from '../types';
import {
getSeedlessOnboardingControllerMessenger,
SeedlessOnboardingControllerMessenger,
} from '../messengers/seedless-onboarding';
import { buildControllerInitRequestMock } from '../test/utils';
import { SeedlessOnboardingControllerInit } from './seedless-onboarding-controller-init';

jest.mock('@metamask/seedless-onboarding-controller');

function buildInitRequestMock(): jest.Mocked<
ControllerInitRequest<SeedlessOnboardingControllerMessenger>
> {
const baseControllerMessenger = new Messenger();

return {
...buildControllerInitRequestMock(),
controllerMessenger: getSeedlessOnboardingControllerMessenger(
baseControllerMessenger,
),
initMessenger: undefined,
};
}

describe('SeedlessOnboardingControllerInit', () => {
const SeedlessOnboardingControllerClassMock = jest.mocked(
SeedlessOnboardingController,
);

beforeEach(() => {
jest.resetAllMocks();
});

it('should return controller instance', () => {
const requestMock = buildInitRequestMock();
expect(
SeedlessOnboardingControllerInit(requestMock).controller,
).toBeInstanceOf(SeedlessOnboardingController);
});

it('initializes with correct messenger and state', () => {
const requestMock = buildInitRequestMock();
SeedlessOnboardingControllerInit(requestMock);

const network = process.env.WEB3AUTH_NETWORK as Web3AuthNetwork;

expect(SeedlessOnboardingControllerClassMock).toHaveBeenCalledWith({
messenger: requestMock.controllerMessenger,
state: requestMock.persistedState.SeedlessOnboardingController,
network,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
SeedlessOnboardingController,
SeedlessOnboardingControllerMessenger,
Web3AuthNetwork,
} from '@metamask/seedless-onboarding-controller';
import { ControllerInitFunction } from '../types';

export const SeedlessOnboardingControllerInit: ControllerInitFunction<
SeedlessOnboardingController,
SeedlessOnboardingControllerMessenger
> = ({ controllerMessenger, persistedState }) => {
const network = process.env.WEB3AUTH_NETWORK as Web3AuthNetwork;
if (!network) {
throw new Error('WEB3AUTH_NETWORK is not set in the environment');
}

const controller = new SeedlessOnboardingController({
messenger: controllerMessenger,
state: persistedState.SeedlessOnboardingController,
network,
});

return {
controller,
};
};
34 changes: 34 additions & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ import {
import { getIsQuicknodeEndpointUrl } from './lib/network-controller/utils';
import { isRelaySupported } from './lib/transaction/transaction-relay';
import OAuthService from './services/oauth/oauth-service';
import { SeedlessOnboardingControllerInit } from './controller-init/seedless-onboarding/seedless-onboarding-controller-init';

export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
Expand Down Expand Up @@ -1888,6 +1889,7 @@ export default class MetamaskController extends EventEmitter {
NotificationServicesPushControllerInit,
DeFiPositionsController: DeFiPositionsControllerInit,
DelegationController: DelegationControllerInit,
SeedlessOnboardingController: SeedlessOnboardingControllerInit,
};

const {
Expand Down Expand Up @@ -1939,6 +1941,8 @@ export default class MetamaskController extends EventEmitter {
this.notificationServicesPushController =
controllersByName.NotificationServicesPushController;
this.deFiPositionsController = controllersByName.DeFiPositionsController;
this.seedlessOnboardingController =
controllersByName.SeedlessOnboardingController;

this.notificationServicesController.init();

Expand Down Expand Up @@ -2113,6 +2117,7 @@ export default class MetamaskController extends EventEmitter {
NetworkController: this.networkController,
AlertController: this.alertController,
OnboardingController: this.onboardingController,
SeedlessOnboardingController: this.seedlessOnboardingController,
PermissionController: this.permissionController,
PermissionLogController: this.permissionLogController,
SubjectMetadataController: this.subjectMetadataController,
Expand Down Expand Up @@ -2166,6 +2171,7 @@ export default class MetamaskController extends EventEmitter {
CurrencyController: this.currencyRateController,
AlertController: this.alertController,
OnboardingController: this.onboardingController,
SeedlessOnboardingController: this.seedlessOnboardingController,
PermissionController: this.permissionController,
PermissionLogController: this.permissionLogController,
SubjectMetadataController: this.subjectMetadataController,
Expand Down Expand Up @@ -3494,6 +3500,9 @@ export default class MetamaskController extends EventEmitter {
getAccountsBySnapId(this.getSnapKeyring.bind(this), snapId),
///: END:ONLY_INCLUDE_IF

// seedless onboarding
startOAuthLogin: this.startOAuthLogin.bind(this),

// hardware wallets
connectHardware: this.connectHardware.bind(this),
forgetDevice: this.forgetDevice.bind(this),
Expand Down Expand Up @@ -4572,6 +4581,31 @@ export default class MetamaskController extends EventEmitter {
}
}

/**
* Login with social login provider and get User Onboarding details.
*
* AuthenticationResult is an object that contains the temporary Auth token for next step of onboarding flow
* and user's onboarding status to indicate whether the user has already completed the seedless onboarding flow.
*
* @param {AuthConnection} provider - social login provider, `google` | `apple`
* @returns {Promise<boolean>} true if user has not completed the seedless onboarding flow, false otherwise
*/
async startOAuthLogin(provider) {
try {
const oAuthLoginResult = await this.oauthService.startOAuthLogin(
provider,
);

const { isNewUser } =
await this.seedlessOnboardingController.authenticate(oAuthLoginResult);

return isNewUser;
} catch (error) {
log.error('Error while starting social login', error);
throw error;
}
}

//=============================================================================
// VAULT / KEYRING RELATED METHODS
//=============================================================================
Expand Down
40 changes: 40 additions & 0 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
} from '@metamask/chain-agnostic-permission';
import { PermissionDoesNotExistError } from '@metamask/permission-controller';
import { KeyringInternalSnapClient } from '@metamask/keyring-internal-snap-client';
import { AuthConnection } from '@metamask/seedless-onboarding-controller';
import { createTestProviderTools } from '../../test/stub/provider';
import {
HardwareDeviceNames,
Expand Down Expand Up @@ -434,6 +435,10 @@ describe('MetaMaskController', () => {
metamaskController.keyringController,
'createNewVaultAndRestore',
);
jest.spyOn(
metamaskController.seedlessOnboardingController,
'authenticate',
);
});

describe('should reset states on first time profile load', () => {
Expand Down Expand Up @@ -674,6 +679,41 @@ describe('MetaMaskController', () => {
});
});

describe('#startOAuthLogin', () => {
it('should start the OAuth login flow', async () => {
const startOAuthLoginSpy = jest
.spyOn(metamaskController.oauthController, 'startOAuthLogin')
.mockResolvedValueOnce({
idTokens: ['mocked-id-token'],
authConnection: AuthConnection.Google,
authConnectionId: 'mocked-auth-connection-id',
groupedAuthConnectionId: 'mocked-grouped-auth-connection-id',
userId: 'mocked-user-id',
socialLoginEmail: '[email protected]',
});
const authenticateSpy = jest
.spyOn(
metamaskController.seedlessOnboardingController,
'authenticate',
)
.mockResolvedValueOnce({
isNewUser: true,
});

await metamaskController.startOAuthLogin(AuthConnection.Google);

expect(startOAuthLoginSpy).toHaveBeenCalledWith(AuthConnection.Google);
expect(authenticateSpy).toHaveBeenCalledWith({
idTokens: ['mocked-id-token'],
authConnection: AuthConnection.Google,
authConnectionId: 'mocked-auth-connection-id',
groupedAuthConnectionId: 'mocked-grouped-auth-connection-id',
userId: 'mocked-user-id',
socialLoginEmail: '[email protected]',
});
});
});

describe('#createNewVaultAndKeychain', () => {
it('can only create new vault on keyringController once', async () => {
const password = 'a-fake-password';
Expand Down
11 changes: 8 additions & 3 deletions shared/constants/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ export enum FirstTimeFlowType {
*/
restore = 'restore',
/**
* When a user logins with Social Login and goes through the Seedless Onboarding flow,
* they will have the 'seedless' firstTimeFlowType.
* When a user logins with Social Login and creates a new wallet,
* they will have the 'socialCreate' firstTimeFlowType.
*/
seedless = 'seedless',
socialCreate = 'socialCreate',
/**
* When a user logins with Social Login and imports their wallet,
* they will have the 'socialImport' firstTimeFlowType.
*/
socialImport = 'socialImport',
}
23 changes: 19 additions & 4 deletions ui/pages/onboarding-flow/account-exist/account-exist.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import thunk from 'redux-thunk';
import { fireEvent, renderWithProvider } from '../../../../test/jest';
import initializedMockState from '../../../../test/data/mock-state.json';
import { FirstTimeFlowType } from '../../../../shared/constants/onboarding';
import { UNLOCK_ROUTE } from '../../../helpers/constants/routes';
import { ONBOARDING_UNLOCK_ROUTE } from '../../../helpers/constants/routes';
import CreationSuccessful from './account-exist';

const mockHistoryPush = jest.fn();
Expand All @@ -26,7 +26,7 @@ describe('Account Exist Seedless Onboarding View', () => {
...initializedMockState,
metamask: {
...initializedMockState.metamask,
firstTimeFlowType: FirstTimeFlowType.seedless,
firstTimeFlowType: FirstTimeFlowType.socialCreate,
},
};
const customMockStore = configureMockStore([thunk])(
Expand All @@ -46,9 +46,24 @@ describe('Account Exist Seedless Onboarding View', () => {
});

it('should navigate to the unlock page when the button is clicked', () => {
const { getByText } = renderWithProvider(<CreationSuccessful />);
const importFirstTimeFlowState = {
...initializedMockState,
metamask: {
...initializedMockState.metamask,
firstTimeFlowType: FirstTimeFlowType.socialCreate,
},
};
const customMockStore = configureMockStore([thunk])(
importFirstTimeFlowState,
);

const { getByText } = renderWithProvider(
<CreationSuccessful />,
customMockStore,
);

const loginButton = getByText('Log in');
fireEvent.click(loginButton);
expect(mockHistoryPush).toHaveBeenCalledWith(UNLOCK_ROUTE);
expect(mockHistoryPush).toHaveBeenCalledWith(ONBOARDING_UNLOCK_ROUTE);
});
});
Loading
Loading