Skip to content

Commit ffe991f

Browse files
authored
fix: agentic qrlogin screen teardown and dev/uat url (#32874)
## **Description** <!-- mms-check: type=text required=true --> This PR hardens the Agentic CLI QR login dashboard WebView flow and updates non-prod dashboard hosts. 1. **Screen teardown cleanup** — Replaced a `useEffect` cleanup keyed on `rejectOnce` with a React Navigation `beforeRemove` listener. The old cleanup also ran when unrelated dependencies changed (e.g. when `webViewUrl` was set), which made it look like the screen unmounted before approve was processed. `beforeRemove` runs only when the screen is actually removed (header back, swipe dismiss, hardware back, `goBack` after approve), and rejects the pending `open()` promise unless approval already completed. 2. **Timeout dismissal** — Re-enabled `dismissOpenWebview()` when the dashboard approval request times out so the modal is popped after rejection. 3. **Dev/UAT dashboard URLs** — Pointed dev and UAT Agentic CLI approval/dashboard WebViews from legacy Web3Auth hosts to `develop-developer.metamask.io` and `staging-developer.metamask.io`, with matching origin allowlist updates. Production URLs are unchanged. ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> CHANGELOG entry: Fixed Agentic CLI dashboard approval not rejecting the pending request when the WebView is dismissed before completion. ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Feature: Agentic CLI QR login dashboard WebView Scenario: Approve dashboard connection Given a dev or UAT build with Agentic CLI QR login enabled And the wallet is unlocked When the user scans an Agentic CLI connect QR code And approves the connection in the dashboard WebView Then the WebView closes And the CLI connection completes successfully Scenario: Dismiss dashboard WebView before approving Given the Agentic CLI dashboard WebView is open When the user swipes to dismiss the modal (or presses hardware back on Android) Then the WebView closes And the pending dashboard approval promise is rejected (CLI link does not hang) Scenario: Header back dismisses dashboard WebView Given the Agentic CLI dashboard WebView is open When the user taps the header back button Then the WebView closes And the pending request is rejected once (no double reject) ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> - [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. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. <!-- Generated with the help of the pr-description AI skill --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes WebView teardown and which origins/hosts load for Agentic CLI in dev/UAT; wrong behavior could hang or prematurely abort CLI linking, but production URLs are unchanged. > > **Overview** > Fixes Agentic CLI dashboard WebView teardown so dismissing the screen does not reject the pending approval at the wrong time, and moves dev/UAT dashboard traffic to MetaMask developer hosts. > > **Dashboard dismiss handling** — Replaces a `useEffect` cleanup tied to `rejectOnce` with a React Navigation `beforeRemove` listener that rejects the pending `open()` promise only when the screen is actually removed and approval has not completed. New component tests cover dismiss-before-approve and no double-reject after success. > > **Non-prod URLs and allowlists** — Dev and UAT builds now use `develop-developer.metamask.io` and `staging-developer.metamask.io` instead of legacy Web3Auth dashboard hosts in `agenticCliConfig`, `AgenticCliApprovalService`, and related tests. WebView origin allowlists add those hosts so in-app navigation stays permitted. Production `developer.metamask.io` is unchanged. > > **QR login tests** — Expectations and copy align with the new hosts; adds coverage for `MM_DEV_API_ENV=prod` using the production auth API and dashboard URL. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 620bbc6. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 3ff2b62 commit ffe991f

7 files changed

Lines changed: 96 additions & 22 deletions

File tree

app/components/Views/AgenticCliApproval/AgenticCliApprovalService.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,16 @@ describe('AgenticCliApprovalService', () => {
8181
});
8282

8383
describe('getApprovalHost', () => {
84-
it('returns the test dashboard host for development builds', () => {
84+
it('returns the develop developer dashboard host for development builds', () => {
8585
mockGetBuildType.mockReturnValue('development');
8686

87-
expect(getApprovalHost()).toBe('https://test-dashboard.web3auth.io');
87+
expect(getApprovalHost()).toBe('https://develop-developer.metamask.io');
8888
});
8989

90-
it('returns the dev dashboard host for UAT builds', () => {
90+
it('returns the staging developer dashboard host for UAT builds', () => {
9191
mockGetBuildType.mockReturnValue('main_uat');
9292

93-
expect(getApprovalHost()).toBe('https://dev-dashboard.web3auth.io');
93+
expect(getApprovalHost()).toBe('https://staging-developer.metamask.io');
9494
});
9595

9696
it('returns the developer dashboard host for production builds', () => {
@@ -99,10 +99,10 @@ describe('AgenticCliApprovalService', () => {
9999
expect(getApprovalHost()).toBe('https://developer.metamask.io');
100100
});
101101

102-
it('returns the test dashboard host for dev channel builds', () => {
102+
it('returns the develop developer dashboard host for dev channel builds', () => {
103103
mockGetBuildType.mockReturnValue('main_dev');
104104

105-
expect(getApprovalHost()).toBe('https://test-dashboard.web3auth.io');
105+
expect(getApprovalHost()).toBe('https://develop-developer.metamask.io');
106106
});
107107
});
108108

@@ -312,7 +312,7 @@ describe('AgenticCliApprovalService', () => {
312312
expect(new URL(url).searchParams.get('mimirSignature')).toBe(
313313
'sig/with+chars',
314314
);
315-
expect(new URL(url).host).toBe('test-dashboard.web3auth.io');
315+
expect(new URL(url).host).toBe('develop-developer.metamask.io');
316316
});
317317
});
318318

app/components/Views/AgenticCliApproval/AgenticCliApprovalService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ const APPROVAL_PAGE_PATH_PATTERN = /^\/agentic\/[a-zA-Z0-9/_-]+$/;
2525
const CLI_DASHBOARD_TOKEN_PATH = '/api/v2/mm-qr-login/token';
2626

2727
const AGENTIC_CLI_APPROVAL_HOST = {
28-
dev: 'https://test-dashboard.web3auth.io',
29-
uat: 'https://dev-dashboard.web3auth.io',
28+
dev: 'https://develop-developer.metamask.io',
29+
uat: 'https://staging-developer.metamask.io',
3030
prod: 'https://developer.metamask.io',
3131
} as const;
3232

@@ -41,6 +41,8 @@ interface CliDashboardTokenResponse {
4141
const ALLOWED_ORIGIN_PATTERNS: RegExp[] = [
4242
/^https:\/\/link\.metamask\.io$/,
4343
/^https:\/\/developer\.metamask\.io$/,
44+
/^https:\/\/develop-developer\.metamask\.io$/,
45+
/^https:\/\/staging-developer\.metamask\.io$/,
4446
/^https:\/\/test-dashboard\.web3auth\.io$/,
4547
/^https:\/\/dev-dashboard\.web3auth\.io$/,
4648
/^https:\/\/auth\.web3auth\.io$/,

app/components/Views/AgenticCliDashboardWebview/AgenticCliDashboardWebviewService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const ALLOWED_ORIGIN_PATTERNS: RegExp[] = [
1515
/^https:\/\/auth\.web3auth\.io$/,
1616
/^https:\/\/[a-z0-9-]+\.cx\.metamask\.io$/,
1717
/^https:\/\/developer\.metamask\.io$/,
18+
/^https:\/\/develop-developer\.metamask\.io$/,
19+
/^https:\/\/staging-developer\.metamask\.io$/,
1820
];
1921

2022
/** Non-prod dashboard hosts used when `MM_DEV_API_ENV=dev`. */

app/components/Views/AgenticCliDashboardWebview/index.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Logger from '../../../util/Logger';
88
const mockGoBack = jest.fn();
99
const mockSetOptions = jest.fn();
1010
const mockUseParams = jest.fn();
11+
const mockBeforeRemoveCallback = { current: null as (() => void) | null };
1112
const mockBuildWebViewUrl =
1213
AgenticCliDashboardWebviewService.buildWebViewUrl as jest.Mock;
1314
const mockParseEvent =
@@ -31,6 +32,12 @@ jest.mock('@react-navigation/native', () => ({
3132
useNavigation: () => ({
3233
goBack: mockGoBack,
3334
setOptions: mockSetOptions,
35+
addListener: (event: string, callback: () => void) => {
36+
if (event === 'beforeRemove') {
37+
mockBeforeRemoveCallback.current = callback;
38+
}
39+
return jest.fn();
40+
},
3441
}),
3542
}));
3643

@@ -130,6 +137,7 @@ describe('AgenticCliDashboardWebview', () => {
130137
beforeEach(() => {
131138
jest.useRealTimers();
132139
jest.clearAllMocks();
140+
mockBeforeRemoveCallback.current = null;
133141
jest.replaceProperty(Platform, 'OS', 'ios');
134142
mockUseParams.mockReturnValue(params);
135143
mockBuildWebViewUrl.mockReturnValue(
@@ -179,6 +187,43 @@ describe('AgenticCliDashboardWebview', () => {
179187
expect(mockGoBack).toHaveBeenCalledTimes(1);
180188
});
181189

190+
it('rejects the pending request when beforeRemove fires without prior completion', async () => {
191+
render(<AgenticCliDashboardWebview />);
192+
await waitFor(() =>
193+
expect(mockBeforeRemoveCallback.current).toEqual(expect.any(Function)),
194+
);
195+
196+
act(() => {
197+
mockBeforeRemoveCallback.current?.();
198+
});
199+
200+
expect(mockReject).toHaveBeenCalledWith(
201+
'request-1',
202+
expect.objectContaining({ message: 'Dashboard approval closed.' }),
203+
);
204+
expect(mockReject).toHaveBeenCalledTimes(1);
205+
});
206+
207+
it('does not reject again when beforeRemove fires after approval', async () => {
208+
render(<AgenticCliDashboardWebview />);
209+
await waitFor(() => expect(mockWebViewProps).toHaveBeenCalled());
210+
mockParseEvent.mockReturnValue({ type: 'approved', cliToken: 'cli-token' });
211+
212+
act(() => {
213+
getLatestWebViewProps().onMessage({
214+
nativeEvent: { data: 'message' },
215+
});
216+
});
217+
218+
await waitFor(() => expect(mockResolve).toHaveBeenCalledTimes(1));
219+
220+
act(() => {
221+
mockBeforeRemoveCallback.current?.();
222+
});
223+
224+
expect(mockReject).not.toHaveBeenCalled();
225+
});
226+
182227
it('rejects and closes when the dashboard posts close', async () => {
183228
render(<AgenticCliDashboardWebview />);
184229
await waitFor(() => expect(mockWebViewProps).toHaveBeenCalled());

app/components/Views/AgenticCliDashboardWebview/index.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,14 @@ const AgenticCliDashboardWebview: React.FC = () => {
110110
);
111111
}, [close, navigation]);
112112

113-
useEffect(
114-
() => () => {
113+
useEffect(() => {
114+
const unsubscribe = navigation.addListener('beforeRemove', () => {
115+
if (completedRef.current) return;
115116
rejectOnce(DASHBOARD_CLOSED_MESSAGE);
116-
},
117-
[rejectOnce],
118-
);
117+
});
118+
119+
return unsubscribe;
120+
}, [navigation, rejectOnce]);
119121

120122
const handleMessage = useCallback(
121123
(messageEvent: WebViewMessageEvent) => {

app/core/AgenticCli/AgenticCliQrLoginService.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ describe('AgenticCliQrLoginService', () => {
236236
expect(setStage).toHaveBeenCalledWith('send-auth-token-to-cli');
237237
});
238238

239-
it('uses dev auth API and test dashboard for main_dev builds', async () => {
239+
it('uses dev auth API and develop dashboard for main_dev builds', async () => {
240240
const { handleAgenticCliQrLogin } = loadAgenticCliQrLogin('main_dev');
241241
const conn = createMockConnection();
242242

@@ -253,12 +253,12 @@ describe('AgenticCliQrLoginService', () => {
253253
expect.any(Object),
254254
);
255255
expect(AgenticCliDashboardWebviewService.open).toHaveBeenCalledWith({
256-
dashboardUrl: 'https://test-dashboard.web3auth.io/agentic/login',
256+
dashboardUrl: 'https://develop-developer.metamask.io/agentic/login',
257257
dashboardToken: 'dashboard-token',
258258
});
259259
});
260260

261-
it('uses UAT auth API and UAT dashboard for main_uat builds', async () => {
261+
it('uses dev auth API and staging dashboard for main_uat builds', async () => {
262262
const { handleAgenticCliQrLogin } = loadAgenticCliQrLogin('main_uat');
263263
const conn = createMockConnection();
264264

@@ -275,7 +275,30 @@ describe('AgenticCliQrLoginService', () => {
275275
expect.any(Object),
276276
);
277277
expect(AgenticCliDashboardWebviewService.open).toHaveBeenCalledWith({
278-
dashboardUrl: 'https://dev-dashboard.web3auth.io/agentic/login',
278+
dashboardUrl: 'https://staging-developer.metamask.io/agentic/login',
279+
dashboardToken: 'dashboard-token',
280+
});
281+
});
282+
283+
it('uses prod auth API when MM_DEV_API_ENV is prod', async () => {
284+
process.env.MM_DEV_API_ENV = 'prod';
285+
const { handleAgenticCliQrLogin } = loadAgenticCliQrLogin('main_prod');
286+
const conn = createMockConnection();
287+
288+
await handleAgenticCliQrLogin({
289+
connReq: mockConnectionRequest({ name: 'agentic-cli' }),
290+
conn,
291+
setStage: jest.fn(),
292+
cleanupConnection: jest.fn().mockResolvedValue(undefined),
293+
});
294+
295+
expect(mockGetEnvUrls).toHaveBeenCalledWith('prd');
296+
expect(global.fetch).toHaveBeenCalledWith(
297+
'https://authentication.api.cx.metamask.io/api/v2/mm-qr-login/token',
298+
expect.any(Object),
299+
);
300+
expect(AgenticCliDashboardWebviewService.open).toHaveBeenCalledWith({
301+
dashboardUrl: 'https://developer.metamask.io/agentic/login',
279302
dashboardToken: 'dashboard-token',
280303
});
281304
});

app/core/AgenticCli/agenticCliConfig.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ export const CLI_DASHBOARD_TOKEN_PATH = '/api/v2/mm-qr-login/token';
77
export const ENGINE_READY_POLL_MS = 250;
88

99
export const DASHBOARD_WEBVIEW_URL_BY_ENV: Record<string, string> = {
10-
main_dev: 'https://test-dashboard.web3auth.io/agentic/login',
11-
main_uat: 'https://dev-dashboard.web3auth.io/agentic/login',
10+
main_dev: 'https://develop-developer.metamask.io/agentic/login',
11+
main_uat: 'https://staging-developer.metamask.io/agentic/login',
1212
main_prod: 'https://developer.metamask.io/agentic/login',
13-
flask_dev: 'https://test-dashboard.web3auth.io/agentic/login',
14-
flask_uat: 'https://dev-dashboard.web3auth.io/agentic/login',
13+
flask_dev: 'https://develop-developer.metamask.io/agentic/login',
14+
flask_uat: 'https://staging-developer.metamask.io/agentic/login',
1515
flask_prod: 'https://developer.metamask.io/agentic/login',
1616
};
1717

0 commit comments

Comments
 (0)