Skip to content

Commit e270194

Browse files
rafiki270claude
andcommitted
feat(ios): implement JavaScript dialog interception via WKUIDelegate
Replace dialog stubs in BrowserManagementHandler with real dialog state tracking. Adds DialogState to intercept alert/confirm/prompt from WKWebView, with auto-handler support for LLM workflows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3a40d19 commit e270194

5 files changed

Lines changed: 170 additions & 7 deletions

File tree

apps/ios/Kelpie.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
A100000004 /* URLBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000004 /* URLBarView.swift */; };
2020
A100000005 /* WebViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000005 /* WebViewCoordinator.swift */; };
2121
A100000006 /* BrowserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000006 /* BrowserState.swift */; };
22+
A100000067 /* DialogState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000067 /* DialogState.swift */; };
2223
A100000055 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000055 /* FeatureFlags.swift */; };
2324
A100000056 /* Snapshot3DBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000056 /* Snapshot3DBridge.swift */; };
2425
A100000057 /* Snapshot3DHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B100000057 /* Snapshot3DHandler.swift */; };
@@ -85,6 +86,7 @@
8586
B100000004 /* URLBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBarView.swift; sourceTree = "<group>"; };
8687
B100000005 /* WebViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewCoordinator.swift; sourceTree = "<group>"; };
8788
B100000006 /* BrowserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserState.swift; sourceTree = "<group>"; };
89+
B100000067 /* DialogState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogState.swift; sourceTree = "<group>"; };
8890
B100000055 /* FeatureFlags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlags.swift; sourceTree = "<group>"; };
8991
B100000056 /* Snapshot3DBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Snapshot3DBridge.swift; path = ../macos/Kelpie/Handlers/Snapshot3DBridge.swift; sourceTree = SOURCE_ROOT; };
9092
B100000057 /* Snapshot3DHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Snapshot3DHandler.swift; sourceTree = "<group>"; };
@@ -200,6 +202,7 @@
200202
children = (
201203
B100000005 /* WebViewCoordinator.swift */,
202204
B100000006 /* BrowserState.swift */,
205+
B100000067 /* DialogState.swift */,
203206
B100000055 /* FeatureFlags.swift */,
204207
B100000034 /* SafariAuthHelper.swift */,
205208
4372587B096643C1AE55184F /* BookmarkStore.swift */,
@@ -379,6 +382,7 @@
379382
A100000004 /* URLBarView.swift in Sources */,
380383
A100000005 /* WebViewCoordinator.swift in Sources */,
381384
A100000006 /* BrowserState.swift in Sources */,
385+
A100000067 /* DialogState.swift in Sources */,
382386
A100000055 /* FeatureFlags.swift in Sources */,
383387
A100000007 /* HTTPServer.swift in Sources */,
384388
A100000008 /* Router.swift in Sources */,
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Foundation
2+
3+
/// Tracks pending JavaScript dialogs (alert/confirm/prompt) from WKWebView.
4+
@MainActor
5+
final class DialogState {
6+
7+
enum DialogType: String {
8+
case alert
9+
case confirm
10+
case prompt
11+
}
12+
13+
struct PendingDialog {
14+
let type: DialogType
15+
let message: String
16+
let defaultText: String?
17+
let completion: (String?) -> Void // nil = dismiss, non-nil = accept
18+
}
19+
20+
/// The dialog currently waiting for a response, or nil if none is showing.
21+
private(set) var current: PendingDialog?
22+
23+
/// Auto-handler mode: nil = queue dialogs for manual handling,
24+
/// "accept" = auto-accept, "dismiss" = auto-dismiss.
25+
var autoHandler: String?
26+
27+
/// Default text to send for prompt dialogs when auto-accepting.
28+
var autoPromptText: String = ""
29+
30+
/// Enqueue a dialog. If autoHandler is set, resolve immediately; otherwise hold it as current.
31+
func enqueue(_ dialog: PendingDialog) {
32+
if let mode = autoHandler {
33+
resolve(dialog, action: mode)
34+
return
35+
}
36+
// If a dialog is already pending, dismiss it to avoid hanging the WebView.
37+
if let existing = current {
38+
existing.completion(nil)
39+
}
40+
current = dialog
41+
}
42+
43+
/// Handle the current dialog with an explicit action.
44+
func handle(action: String, text: String? = nil) -> (type: DialogType, handled: Bool) {
45+
guard let dialog = current else {
46+
return (.alert, false)
47+
}
48+
current = nil
49+
if action == "accept" {
50+
let responseText: String
51+
if dialog.type == .prompt {
52+
responseText = text ?? dialog.defaultText ?? ""
53+
} else {
54+
responseText = ""
55+
}
56+
dialog.completion(responseText)
57+
} else {
58+
dialog.completion(nil)
59+
}
60+
return (dialog.type, true)
61+
}
62+
63+
// MARK: - Private
64+
65+
private func resolve(_ dialog: PendingDialog, action: String) {
66+
if action == "accept" {
67+
let responseText: String
68+
if dialog.type == .prompt {
69+
responseText = autoPromptText.isEmpty ? (dialog.defaultText ?? "") : autoPromptText
70+
} else {
71+
responseText = ""
72+
}
73+
dialog.completion(responseText)
74+
} else {
75+
dialog.completion(nil)
76+
}
77+
}
78+
}

apps/ios/Kelpie/Browser/WebViewCoordinator.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,39 @@ struct WebViewContainer: UIViewRepresentable {
141141
return nil
142142
}
143143

144+
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
145+
guard let dialogState = handlerContext?.dialogState else {
146+
completionHandler()
147+
return
148+
}
149+
let dialog = DialogState.PendingDialog(type: .alert, message: message, defaultText: nil) { _ in
150+
completionHandler()
151+
}
152+
dialogState.enqueue(dialog)
153+
}
154+
155+
func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
156+
guard let dialogState = handlerContext?.dialogState else {
157+
completionHandler(false)
158+
return
159+
}
160+
let dialog = DialogState.PendingDialog(type: .confirm, message: message, defaultText: nil) { result in
161+
completionHandler(result != nil)
162+
}
163+
dialogState.enqueue(dialog)
164+
}
165+
166+
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
167+
guard let dialogState = handlerContext?.dialogState else {
168+
completionHandler(nil)
169+
return
170+
}
171+
let dialog = DialogState.PendingDialog(type: .prompt, message: prompt, defaultText: defaultText) { result in
172+
completionHandler(result)
173+
}
174+
dialogState.enqueue(dialog)
175+
}
176+
144177
private func recordMainDocumentResponse(_ navigationResponse: WKNavigationResponse) {
145178
guard navigationResponse.isForMainFrame,
146179
let response = navigationResponse.response as? HTTPURLResponse,

apps/ios/Kelpie/Handlers/BrowserManagementHandler.swift

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,9 @@ struct BrowserManagementHandler {
4444
router.register("get-iframe-context") { _ in successResponse(["context": "main"]) }
4545

4646
// Dialogs
47-
router.register("get-dialog") { _ in successResponse(["showing": false, "dialog": NSNull()]) }
48-
router.register("handle-dialog") { body in
49-
successResponse(["action": body["action"] ?? "accept", "dialogType": "none"])
50-
}
51-
router.register("set-dialog-auto-handler") { body in
52-
successResponse(["enabled": body["enabled"] ?? true])
53-
}
47+
router.register("get-dialog") { _ in await getDialog() }
48+
router.register("handle-dialog") { body in await handleDialog(body) }
49+
router.register("set-dialog-auto-handler") { body in await setDialogAutoHandler(body) }
5450

5551
// Tabs (stub — full implementation needs TabManager)
5652
router.register("get-tabs") { _ in await getTabs() }
@@ -284,6 +280,57 @@ struct BrowserManagementHandler {
284280
return successResponse(["iframe": ["id": id, "src": ""], "context": "iframe"])
285281
}
286282

283+
// MARK: - Dialogs
284+
285+
@MainActor
286+
private func getDialog() async -> [String: Any] {
287+
let state = context.dialogState
288+
guard let dialog = state.current else {
289+
return successResponse(["showing": false, "dialog": NSNull()])
290+
}
291+
var info: [String: Any] = [
292+
"type": dialog.type.rawValue,
293+
"message": dialog.message
294+
]
295+
if let defaultText = dialog.defaultText {
296+
info["defaultValue"] = defaultText
297+
} else {
298+
info["defaultValue"] = NSNull()
299+
}
300+
return successResponse(["showing": true, "dialog": info])
301+
}
302+
303+
@MainActor
304+
private func handleDialog(_ body: [String: Any]) async -> [String: Any] {
305+
let action = body["action"] as? String ?? "accept"
306+
let text = body["promptText"] as? String ?? body["text"] as? String
307+
let result = context.dialogState.handle(action: action, text: text)
308+
guard result.handled else {
309+
return errorResponse(code: "NO_DIALOG", message: "No dialog is currently showing")
310+
}
311+
return successResponse(["action": action, "dialogType": result.type.rawValue])
312+
}
313+
314+
@MainActor
315+
private func setDialogAutoHandler(_ body: [String: Any]) async -> [String: Any] {
316+
let state = context.dialogState
317+
let enabled = body["enabled"] as? Bool ?? true
318+
let defaultAction = body["defaultAction"] as? String ?? "accept"
319+
320+
if enabled {
321+
if defaultAction == "queue" {
322+
state.autoHandler = nil
323+
} else {
324+
state.autoHandler = defaultAction
325+
}
326+
} else {
327+
state.autoHandler = nil
328+
}
329+
330+
state.autoPromptText = body["promptText"] as? String ?? ""
331+
return successResponse(["enabled": enabled])
332+
}
333+
287334
// MARK: - Tabs
288335

289336
@MainActor

apps/ios/Kelpie/Handlers/HandlerContext.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ final class HandlerContext: NSObject, WKScriptMessageHandler {
1010
var isIn3DInspector = false
1111
var scriptPlaybackState: ScriptPlaybackState?
1212
let safariAuth = SafariAuthHelper()
13+
let dialogState = DialogState()
1314

1415
override nonisolated init() { super.init() }
1516

0 commit comments

Comments
 (0)