Skip to content

Commit 9541dda

Browse files
feat(agentic-cli): add QR login dashboard webview
1 parent 1cddbdf commit 9541dda

15 files changed

Lines changed: 868 additions & 78 deletions

app/components/Nav/App/App.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import OptinMetrics from '../../UI/OptinMetrics';
2222
import OnboardingInterestQuestionnaire from '../../Views/OnboardingInterestQuestionnaire';
2323
import SimpleWebview from '../../Views/SimpleWebview';
2424
import CliAuthWebview from '../../Views/SDK/CliAuthWebview/CliAuthWebview';
25+
import AgenticCliDashboardWebview from '../../Views/AgenticCliDashboardWebview';
2526
import Logger from '../../../util/Logger';
2627
import { useSelector } from 'react-redux';
2728
import {
@@ -1228,6 +1229,16 @@ const AppFlow = () => {
12281229
name={Routes.CONFIRMATION_PAY_WITH_BOTTOM_SHEET}
12291230
component={PayWithBottomSheet}
12301231
/>
1232+
<Stack.Screen
1233+
name={Routes.AGENTIC_CLI_DASHBOARD_WEBVIEW.CONFIRM}
1234+
component={AgenticCliDashboardWebview}
1235+
options={{
1236+
headerShown: true,
1237+
gestureEnabled: true,
1238+
presentation: 'modal',
1239+
cardStyle: { backgroundColor: importedColors.white },
1240+
}}
1241+
/>
12311242
</Stack.Navigator>
12321243
);
12331244
};
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import NavigationService from '../../../core/NavigationService';
2+
import Routes from '../../../constants/navigation/Routes';
3+
import {
4+
AGENTIC_CLI_DASHBOARD_MESSAGE_SOURCE,
5+
type AgenticCliDashboardWebviewParams,
6+
type DashboardWebviewResult,
7+
} from './types';
8+
9+
const MAX_MESSAGE_LENGTH = 16 * 1024;
10+
const WEBVIEW_TIMEOUT_MS = 5 * 60 * 1000;
11+
12+
const ALLOWED_ORIGIN_PATTERNS: RegExp[] = [
13+
/^https:\/\/dashboard\.w3a\.io$/,
14+
/^https:\/\/test-dashboard\.web3auth\.io$/,
15+
/^https:\/\/dev-dashboard\.web3auth\.io$/,
16+
/^https:\/\/[a-z0-9-]+\.cx\.metamask\.io$/,
17+
/^https:\/\/[a-z0-9-]+\.ngrok-free\.app$/,
18+
/^http:\/\/10\.0\.2\.2(?::\d+)?$/,
19+
/^http:\/\/localhost(?::\d+)?$/,
20+
];
21+
22+
interface PendingRequest {
23+
resolve: (cliToken: string) => void;
24+
reject: (error: Error) => void;
25+
timeoutId: ReturnType<typeof setTimeout>;
26+
}
27+
28+
const pendingRequests = new Map<string, PendingRequest>();
29+
30+
const createRequestId = (): string =>
31+
`agentic-cli-dashboard-${Date.now()}-${Math.random().toString(36).slice(2)}`;
32+
33+
const normalizeToken = (value: unknown): string | null =>
34+
typeof value === 'string' && value.trim() ? value : null;
35+
36+
const completeRequest = (
37+
requestId: string,
38+
action: (pending: PendingRequest) => void,
39+
): void => {
40+
const pending = pendingRequests.get(requestId);
41+
if (!pending) return;
42+
clearTimeout(pending.timeoutId);
43+
pendingRequests.delete(requestId);
44+
action(pending);
45+
};
46+
47+
export const AgenticCliDashboardWebviewService = {
48+
buildWebViewUrl(dashboardUrl: string, dashboardToken: string): string {
49+
const url = new URL(dashboardUrl);
50+
51+
const origin = `${url.protocol}//${url.host}`;
52+
53+
if (!AgenticCliDashboardWebviewService.isOriginAllowed(origin)) {
54+
throw new Error('Dashboard origin is not allowed');
55+
}
56+
57+
url.searchParams.set('auth_token', dashboardToken);
58+
return url.toString();
59+
},
60+
61+
isOriginAllowed(origin: string): boolean {
62+
return ALLOWED_ORIGIN_PATTERNS.some((re) => re.test(origin));
63+
},
64+
65+
shouldLoadInWebView(url: string): boolean {
66+
try {
67+
const parsed = new URL(url);
68+
return AgenticCliDashboardWebviewService.isOriginAllowed(
69+
`${parsed.protocol}//${parsed.host}`,
70+
);
71+
} catch {
72+
return false;
73+
}
74+
},
75+
76+
parseEvent(raw: unknown): DashboardWebviewResult | null {
77+
if (typeof raw !== 'string') return null;
78+
if (raw.length > MAX_MESSAGE_LENGTH) return null;
79+
80+
let parsed: unknown;
81+
try {
82+
parsed = JSON.parse(raw);
83+
} catch {
84+
const cliToken = normalizeToken(raw);
85+
return cliToken ? { type: 'approved', cliToken } : null;
86+
}
87+
88+
if (!parsed || typeof parsed !== 'object') return null;
89+
90+
const obj = parsed as Record<string, unknown>;
91+
const source = obj.source;
92+
if (
93+
source !== undefined &&
94+
source !== AGENTIC_CLI_DASHBOARD_MESSAGE_SOURCE
95+
) {
96+
return null;
97+
}
98+
99+
const type = typeof obj.type === 'string' ? obj.type : 'approved';
100+
const cliToken =
101+
normalizeToken(obj.cli_token) ??
102+
normalizeToken(obj.cliToken) ??
103+
normalizeToken(obj.token);
104+
105+
switch (type) {
106+
case 'approved':
107+
case 'approve':
108+
case 'success':
109+
return cliToken ? { type: 'approved', cliToken } : null;
110+
case 'rejected':
111+
case 'reject':
112+
case 'denied':
113+
case 'deny':
114+
return {
115+
type: 'rejected',
116+
message:
117+
typeof obj.message === 'string' ? obj.message : 'Request rejected',
118+
};
119+
case 'close':
120+
return {
121+
type: 'close',
122+
message:
123+
typeof obj.message === 'string' ? obj.message : 'WebView closed',
124+
};
125+
case 'error':
126+
return {
127+
type: 'error',
128+
message:
129+
typeof obj.message === 'string' ? obj.message : 'Unknown error',
130+
};
131+
default:
132+
return null;
133+
}
134+
},
135+
136+
open(
137+
params: Omit<AgenticCliDashboardWebviewParams, 'requestId'>,
138+
): Promise<string> {
139+
const requestId = createRequestId();
140+
141+
return new Promise((resolve, reject) => {
142+
const timeoutId = setTimeout(() => {
143+
pendingRequests.delete(requestId);
144+
reject(new Error('Dashboard approval timed out.'));
145+
}, WEBVIEW_TIMEOUT_MS);
146+
147+
pendingRequests.set(requestId, {
148+
resolve,
149+
reject,
150+
timeoutId,
151+
});
152+
153+
try {
154+
NavigationService.navigation.navigate(
155+
Routes.AGENTIC_CLI_DASHBOARD_WEBVIEW.CONFIRM,
156+
{
157+
...params,
158+
requestId,
159+
},
160+
);
161+
} catch (error) {
162+
completeRequest(requestId, (pending) =>
163+
pending.reject(
164+
error instanceof Error ? error : new Error(String(error)),
165+
),
166+
);
167+
}
168+
});
169+
},
170+
171+
resolve(requestId: string, cliToken: string): void {
172+
completeRequest(requestId, (pending) => pending.resolve(cliToken));
173+
},
174+
175+
reject(requestId: string, error: Error): void {
176+
completeRequest(requestId, (pending) => pending.reject(error));
177+
},
178+
};

0 commit comments

Comments
 (0)