Skip to content

Commit 8423995

Browse files
ieowhieu-wsmgvchaitanyapottitommasini
authored
feat: integrate seedless onboarding controller into the Engine (#15844)
fix tests <!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Integration the following social login via the OAuth Service and SeedlessController, Google Apple Proposed Authentication Flow using seedless onboarding ![Screenshot 2025-04-29 at 8 25 46 PM](https://github.com/user-attachments/assets/99f93c54-ebae-4ac9-a62c-069c4c09beb4) This PR mainly integrate the Seedless Controller into Metamask. It does not include the OAuthServices integration and Login UI Flow. The Login UI Flow will be added on top of the new SRP UI Flow <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Related issues** Fixes: ## **Manual testing steps** ``` No manual test required only new controller is added ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --------- Co-authored-by: hieu-w <hieu@tor.us> Co-authored-by: smgv <gpatra1996@gmail.com> Co-authored-by: hieu-w <maestrokoder@gmail.com> Co-authored-by: Chaitanya Potti <chaitanya.potti@gmail.com> Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> Co-authored-by: Curtis David <Curtis.David7@gmail.com>
1 parent 017fed8 commit 8423995

26 files changed

Lines changed: 548 additions & 31 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ app/reducers/settings @MetaMask/wallet-ux
185185
app/components/Views/transactions @MetaMask/transactions
186186

187187
# Web3auth Team
188+
app/core/Engine/messengers/seedless-onboarding-controller-messenger @MetaMask/web3auth
189+
app/core/Engine/controllers/seedless-onboarding-controller @MetaMask/web3auth
188190
app/components/Views/Onboarding @MetaMask/web3auth
189191
app/reducers/onboarding @MetaMask/web3auth
190192

.js.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,7 @@ export MM_BRIDGE_ENABLED="true"
130130
# Set ramps environment to staging.
131131
# This is required when developing or checking existing ramps tests locally.
132132
export RAMP_INTERNAL_BUILD="true"
133+
134+
135+
# set seedless onboarding environment
136+
export WEB3AUTH_NETWORK="sapphire_devnet"

app/components/Views/WalletActions/WalletActions.test.tsx

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -163,23 +163,28 @@ jest.mock('../../UI/Ramp/Aggregator/hooks/useRampNetwork', () => ({
163163
default: jest.fn().mockReturnValue([true]),
164164
}));
165165

166-
jest.mock('../../../core/AppConstants', () => ({
167-
SWAPS: {
168-
ACTIVE: true,
169-
},
170-
BUNDLE_IDS: {
171-
ANDROID: 'io.metamask',
172-
IOS: '1438144202',
173-
},
174-
MM_UNIVERSAL_LINK_HOST: 'metamask.app.link',
175-
WALLET_CONNECT: {
176-
PROJECT_ID: 'test-project-id',
177-
},
178-
BRIDGE: {
179-
ACTIVE: true,
180-
URL: 'https://bridge.metamask.io',
181-
},
182-
}));
166+
jest.mock('../../../core/AppConstants', () => {
167+
const actual = jest.requireActual('../../../core/AppConstants');
168+
169+
return {
170+
...actual,
171+
SWAPS: {
172+
ACTIVE: true,
173+
},
174+
BUNDLE_IDS: {
175+
ANDROID: 'io.metamask',
176+
IOS: '1438144202',
177+
},
178+
MM_UNIVERSAL_LINK_HOST: 'metamask.app.link',
179+
WALLET_CONNECT: {
180+
PROJECT_ID: 'test-project-id',
181+
},
182+
BRIDGE: {
183+
ACTIVE: true,
184+
URL: 'https://bridge.metamask.io',
185+
},
186+
};
187+
});
183188

184189
const mockInitialState: DeepPartial<RootState> = {
185190
swaps: { '0x1': { isLive: true }, hasOnboarded: false, isLive: true },

app/core/AppConstants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ export default {
237237
VERSION: 'v1',
238238
DEFAULT_FETCH_INTERVAL: 15 * 60 * 1000, // 15 minutes
239239
},
240+
SEEDLESS_ONBOARDING: {
241+
WEB3AUTH_NETWORK: process.env.WEB3AUTH_NETWORK,
242+
},
240243
TOKEN_DISCOVERY_BROWSER_ENABLED:
241244
process.env.TOKEN_DISCOVERY_BROWSER_ENABLED === 'true',
242245
EIP_7702_PUBLIC_KEY: '0x3c7a1cCCe462e96D186B8ca9a1BCB2010C3dABa3',

app/core/Engine/Engine.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ import { ErrorReportingService } from '@metamask/error-reporting-service';
228228
import { captureException } from '@sentry/react-native';
229229
import { WebSocketServiceInit } from './controllers/snaps/websocket-service-init';
230230

231+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
232+
import { seedlessOnboardingControllerInit } from './controllers/seedless-onboarding-controller';
233+
///: END:ONLY_INCLUDE_IF
234+
231235
const NON_EMPTY = 'NON_EMPTY';
232236

233237
const encryptor = new Encryptor({
@@ -1258,6 +1262,9 @@ export class Engine {
12581262
MultichainBalancesController: multichainBalancesControllerInit,
12591263
MultichainTransactionsController: multichainTransactionsControllerInit,
12601264
///: END:ONLY_INCLUDE_IF
1265+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
1266+
SeedlessOnboardingController: seedlessOnboardingControllerInit,
1267+
///: END:ONLY_INCLUDE_IF
12611268
},
12621269
persistedState: initialState as EngineState,
12631270
existingControllersByName,
@@ -1271,7 +1278,10 @@ export class Engine {
12711278
const gasFeeController = controllersByName.GasFeeController;
12721279
const signatureController = controllersByName.SignatureController;
12731280
const transactionController = controllersByName.TransactionController;
1274-
1281+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
1282+
const seedlessOnboardingController =
1283+
controllersByName.SeedlessOnboardingController;
1284+
///: END:ONLY_INCLUDE_IF
12751285
// Backwards compatibility for existing references
12761286
this.accountsController = accountsController;
12771287
this.gasFeeController = gasFeeController;
@@ -1625,6 +1635,9 @@ export class Engine {
16251635
BridgeStatusController: bridgeStatusController,
16261636
EarnController: earnController,
16271637
DeFiPositionsController: controllersByName.DeFiPositionsController,
1638+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
1639+
SeedlessOnboardingController: seedlessOnboardingController,
1640+
///: END:ONLY_INCLUDE_IF
16281641
};
16291642

16301643
const childControllers = Object.assign({}, this.context);
@@ -2346,6 +2359,9 @@ export default {
23462359
BridgeStatusController,
23472360
EarnController,
23482361
DeFiPositionsController,
2362+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
2363+
SeedlessOnboardingController,
2364+
///: END:ONLY_INCLUDE_IF
23492365
} = instance.datamodel.state;
23502366

23512367
return {
@@ -2399,6 +2415,9 @@ export default {
23992415
BridgeStatusController,
24002416
EarnController,
24012417
DeFiPositionsController,
2418+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
2419+
SeedlessOnboardingController,
2420+
///: END:ONLY_INCLUDE_IF
24022421
};
24032422
},
24042423

app/core/Engine/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ export const BACKGROUND_STATE_CHANGE_EVENT_NAMES = [
7272
'BridgeStatusController:stateChange',
7373
'EarnController:stateChange',
7474
'DeFiPositionsController:stateChange',
75+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
76+
'SeedlessOnboardingController:stateChange',
77+
///: END:ONLY_INCLUDE_IF
7578
] as const;
7679

7780
export const swapsSupportedChainIds = [
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { seedlessOnboardingControllerInit } from '.';
2+
import { ExtendedControllerMessenger } from '../../../ExtendedControllerMessenger';
3+
import { buildControllerInitRequestMock } from '../../utils/test-utils';
4+
import { ControllerInitRequest } from '../../types';
5+
import {
6+
SeedlessOnboardingController,
7+
SeedlessOnboardingControllerMessenger,
8+
SeedlessOnboardingControllerState,
9+
} from '@metamask/seedless-onboarding-controller';
10+
11+
jest.mock('@metamask/seedless-onboarding-controller', () => {
12+
const actualSeedlessOnboardingController = jest.requireActual(
13+
'@metamask/seedless-onboarding-controller',
14+
);
15+
return {
16+
controllerName: actualSeedlessOnboardingController.controllerName,
17+
getDefaultSeedlessOnboardingControllerState:
18+
actualSeedlessOnboardingController.getDefaultSeedlessOnboardingControllerState,
19+
SeedlessOnboardingController: jest.fn(),
20+
Web3AuthNetwork: actualSeedlessOnboardingController.Web3AuthNetwork,
21+
};
22+
});
23+
24+
describe('seedless onboarding controller init', () => {
25+
const seedlessOnboardingControllerClassMock = jest.mocked(
26+
SeedlessOnboardingController,
27+
);
28+
let initRequestMock: jest.Mocked<
29+
ControllerInitRequest<SeedlessOnboardingControllerMessenger>
30+
>;
31+
32+
beforeEach(() => {
33+
jest.resetAllMocks();
34+
const baseControllerMessenger = new ExtendedControllerMessenger();
35+
// Create controller init request mock
36+
initRequestMock = buildControllerInitRequestMock(baseControllerMessenger);
37+
});
38+
39+
it('returns controller instance', () => {
40+
expect(
41+
seedlessOnboardingControllerInit(initRequestMock).controller,
42+
).toBeInstanceOf(SeedlessOnboardingController);
43+
});
44+
45+
it('controller state should be default state when no initial state is passed in', () => {
46+
const defaultSeedlessOnboardingControllerState = jest
47+
.requireActual('@metamask/seedless-onboarding-controller')
48+
.getDefaultSeedlessOnboardingControllerState();
49+
50+
seedlessOnboardingControllerInit(initRequestMock);
51+
52+
const seedlessOnboardingControllerState =
53+
seedlessOnboardingControllerClassMock.mock.calls[0][0].state;
54+
55+
expect(seedlessOnboardingControllerState).toEqual(
56+
defaultSeedlessOnboardingControllerState,
57+
);
58+
});
59+
60+
it('controller state should be initial state when initial state is passed in', () => {
61+
const initialSeedlessOnboardingControllerState: Partial<SeedlessOnboardingControllerState> =
62+
{
63+
vault: undefined,
64+
nodeAuthTokens: undefined,
65+
};
66+
67+
initRequestMock.persistedState = {
68+
...initRequestMock.persistedState,
69+
SeedlessOnboardingController: initialSeedlessOnboardingControllerState,
70+
};
71+
72+
seedlessOnboardingControllerInit(initRequestMock);
73+
74+
const seedlessOnboardingControllerState =
75+
seedlessOnboardingControllerClassMock.mock.calls[0][0].state;
76+
77+
expect(seedlessOnboardingControllerState).toStrictEqual(
78+
initialSeedlessOnboardingControllerState,
79+
);
80+
});
81+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { ControllerInitFunction } from '../../types';
2+
import {
3+
SeedlessOnboardingController,
4+
SeedlessOnboardingControllerState,
5+
Web3AuthNetwork,
6+
getDefaultSeedlessOnboardingControllerState,
7+
type SeedlessOnboardingControllerMessenger,
8+
} from '@metamask/seedless-onboarding-controller';
9+
import AppConstants from '../../../AppConstants';
10+
import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../../../Encryptor';
11+
import { EncryptionKey } from '../../../Encryptor/types';
12+
13+
const encryptor = new Encryptor({
14+
keyDerivationOptions: LEGACY_DERIVATION_OPTIONS,
15+
});
16+
17+
/**
18+
* Initialize the SeedlessOnboardingController.
19+
*
20+
* @param request - The request object.
21+
* @returns The SeedlessOnboardingController.
22+
*/
23+
export const seedlessOnboardingControllerInit: ControllerInitFunction<
24+
SeedlessOnboardingController<EncryptionKey>,
25+
SeedlessOnboardingControllerMessenger
26+
> = (request) => {
27+
const web3AuthNetwork = AppConstants.SEEDLESS_ONBOARDING.WEB3AUTH_NETWORK;
28+
29+
if (!web3AuthNetwork) {
30+
throw new Error(
31+
`Missing environment variables for SeedlessOnboardingController\n
32+
WEB3AUTH_NETWORK: ${web3AuthNetwork}\n`,
33+
);
34+
}
35+
36+
const { controllerMessenger, persistedState } = request;
37+
38+
const seedlessOnboardingControllerState =
39+
persistedState.SeedlessOnboardingController ??
40+
getDefaultSeedlessOnboardingControllerState();
41+
42+
const controller = new SeedlessOnboardingController({
43+
messenger: controllerMessenger,
44+
state:
45+
seedlessOnboardingControllerState as SeedlessOnboardingControllerState,
46+
encryptor: {
47+
...encryptor,
48+
// Typing issue
49+
decryptWithKey: encryptor.decryptWithKey as unknown as (
50+
key: EncryptionKey,
51+
encryptedString: string,
52+
) => Promise<unknown>,
53+
},
54+
network: web3AuthNetwork as Web3AuthNetwork,
55+
});
56+
57+
return { controller };
58+
};

app/core/Engine/messengers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import { getNotificationServicesControllerMessenger } from './notifications/noti
3333
import { getNotificationServicesPushControllerMessenger } from './notifications/notification-services-push-controller-messenger';
3434
import { getGasFeeControllerMessenger } from './gas-fee-controller-messenger/gas-fee-controller-messenger';
3535
import { getSignatureControllerMessenger } from './signature-controller-messenger';
36+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
37+
import { getSeedlessOnboardingControllerMessenger } from './seedless-onboarding-controller-messenger';
38+
///: END:ONLY_INCLUDE_IF
39+
3640
import { getApprovalControllerMessenger } from './approval-controller-messenger';
3741
/**
3842
* The messengers for the controllers that have been.
@@ -130,4 +134,10 @@ export const CONTROLLER_MESSENGERS = {
130134
getInitMessenger: noop,
131135
},
132136
///: END:ONLY_INCLUDE_IF
137+
///: BEGIN:ONLY_INCLUDE_IF(seedless-onboarding)
138+
SeedlessOnboardingController: {
139+
getMessenger: getSeedlessOnboardingControllerMessenger,
140+
getInitMessenger: noop,
141+
},
142+
///: END:ONLY_INCLUDE_IF
133143
} as const;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { BaseControllerMessenger } from '../../types';
2+
3+
export type SeedlessOnboardingControllerMessenger = ReturnType<
4+
typeof getSeedlessOnboardingControllerMessenger
5+
>;
6+
7+
/**
8+
* Get the SeedlessOnboardingControllerMessenger for the SeedlessOnboardingController.
9+
*
10+
* @param baseControllerMessenger - The base controller messenger.
11+
* @returns The SeedlessOnboardingControllerMessenger.
12+
*/
13+
export function getSeedlessOnboardingControllerMessenger(
14+
baseControllerMessenger: BaseControllerMessenger,
15+
) {
16+
return baseControllerMessenger.getRestricted({
17+
name: 'SeedlessOnboardingController',
18+
allowedEvents: ['KeyringController:lock', 'KeyringController:unlock'],
19+
allowedActions: [],
20+
});
21+
}

0 commit comments

Comments
 (0)