Skip to content
This repository was archived by the owner on Feb 24, 2025. It is now read-only.

Commit a3ab999

Browse files
authored
Change AI Chat web functions (#3880)
Task/Issue URL: https://app.asana.com/0/1201011656765697/1209252104628634/f **Description**: Update methods used by the FE for the duck.ai navigation. No change in behavior
1 parent 7786e33 commit a3ab999

File tree

5 files changed

+170
-9
lines changed

5 files changed

+170
-9
lines changed

DuckDuckGo-iOS.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; };
181181
319A37152829A55F0079FBCE /* AutofillListItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319A37142829A55F0079FBCE /* AutofillListItemTableViewCell.swift */; };
182182
319A37172829C8AD0079FBCE /* UITableViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319A37162829C8AD0079FBCE /* UITableViewExtension.swift */; };
183+
319BE1092D47EBE300E510A4 /* AIChatUserScriptHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319BE1082D47EBE300E510A4 /* AIChatUserScriptHandlerTests.swift */; };
183184
31A42564285A09E800049386 /* FaviconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42563285A09E800049386 /* FaviconView.swift */; };
184185
31A42566285A0A6300049386 /* FaviconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A42565285A0A6300049386 /* FaviconViewModel.swift */; };
185186
31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */; };
@@ -1605,6 +1606,7 @@
16051606
31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = "<group>"; };
16061607
319A37142829A55F0079FBCE /* AutofillListItemTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillListItemTableViewCell.swift; sourceTree = "<group>"; };
16071608
319A37162829C8AD0079FBCE /* UITableViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewExtension.swift; sourceTree = "<group>"; };
1609+
319BE1082D47EBE300E510A4 /* AIChatUserScriptHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatUserScriptHandlerTests.swift; sourceTree = "<group>"; };
16081610
31A42563285A09E800049386 /* FaviconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconView.swift; sourceTree = "<group>"; };
16091611
31A42565285A0A6300049386 /* FaviconViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconViewModel.swift; sourceTree = "<group>"; };
16101612
31B1FA86286EFC5C00CA3C1C /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = "<group>"; };
@@ -3757,6 +3759,7 @@
37573759
310EEA2D2CFFCDB60043CA1A /* AIChat */ = {
37583760
isa = PBXGroup;
37593761
children = (
3762+
319BE1082D47EBE300E510A4 /* AIChatUserScriptHandlerTests.swift */,
37603763
318C5B4C2D302BDB00DAA5FC /* AIChatPayloadHandlerTests.swift */,
37613764
31E77B262D038BB9006F1C9F /* OmnibarAccessoryHandlerTests.swift */,
37623765
310EEA2E2CFFCDBF0043CA1A /* AIChatSettingsTests.swift */,
@@ -8681,6 +8684,7 @@
86818684
564DE4552C3EDEF200D23241 /* ContextualOnboardingNewTabDialogFactoryTests.swift in Sources */,
86828685
6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */,
86838686
981FED7422046017008488D7 /* AutoClearTests.swift in Sources */,
8687+
319BE1092D47EBE300E510A4 /* AIChatUserScriptHandlerTests.swift in Sources */,
86848688
98DDF9F322C4029D00DE38DB /* InitHelpers.swift in Sources */,
86858689
B6AD9E3628D4510A0019CDE9 /* ContentBlockerRulesManagerMock.swift in Sources */,
86868690
851624C52B9602A4002D5CD7 /* HistoryCaptureTests.swift in Sources */,

DuckDuckGo/AIChat/UserScript/AIChatScriptUserValues.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import Foundation
2121

22-
public struct AIChatScriptUserValues: Codable {
22+
public struct AIChatNativeHandoffData: Codable {
2323
let isAIChatHandoffEnabled: Bool
2424
let platform: String
2525
let aiChatPayload: AIChatPayload?
@@ -62,3 +62,8 @@ public struct AIChatScriptUserValues: Codable {
6262
}
6363
}
6464
}
65+
66+
public struct AIChatNativeConfigValues: Codable {
67+
let isAIChatHandoffEnabled: Bool
68+
let platform: String
69+
}

DuckDuckGo/AIChat/UserScript/AIChatUserScript.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ final class AIChatUserScript: NSObject, Subfeature {
2626

2727
enum MessageNames: String, CaseIterable {
2828
case openAIChat
29-
case getUserValues
29+
case getAIChatNativeConfigValues
30+
case getAIChatNativeHandoffData
3031
}
3132

3233
private var handler: AIChatUserScriptHandling
@@ -52,8 +53,10 @@ final class AIChatUserScript: NSObject, Subfeature {
5253

5354
func handler(forMethodNamed methodName: String) -> Subfeature.Handler? {
5455
switch MessageNames(rawValue: methodName) {
55-
case .getUserValues:
56-
return handler.handleGetUserValues
56+
case .getAIChatNativeConfigValues:
57+
return handler.getAIChatNativeConfigValues
58+
case .getAIChatNativeHandoffData:
59+
return handler.getAIChatNativeHandoffData
5760
case .openAIChat:
5861
return handler.openAIChat
5962
default:

DuckDuckGo/AIChat/UserScript/AIChatUserScriptHandling.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import BrowserServicesKit
2222
import RemoteMessaging
2323

2424
protocol AIChatUserScriptHandling {
25-
func handleGetUserValues(params: Any, message: UserScriptMessage) -> Encodable?
25+
func getAIChatNativeConfigValues(params: Any, message: UserScriptMessage) -> Encodable?
26+
func getAIChatNativeHandoffData(params: Any, message: UserScriptMessage) -> Encodable?
2627
func openAIChat(params: Any, message: UserScriptMessage) async -> Encodable?
2728
func setPayloadHandler(_ payloadHandler: (any AIChatPayloadHandling)?)
2829
}
@@ -35,6 +36,14 @@ final class AIChatUserScriptHandler: AIChatUserScriptHandling {
3536
self.featureFlagger = featureFlagger
3637
}
3738

39+
private var isHandoffEnabled: Bool {
40+
featureFlagger.isFeatureOn(.aiChatDeepLink)
41+
}
42+
43+
private var platform: String {
44+
"ios"
45+
}
46+
3847
enum AIChatKeys {
3948
static let aiChatPayload = "aiChatPayload"
4049
}
@@ -58,10 +67,14 @@ final class AIChatUserScriptHandler: AIChatUserScriptHandling {
5867
return nil
5968
}
6069

61-
/// Called when the AI Chat view is displayed. If a payload exists, it retrieves and clears it from storage.
62-
public func handleGetUserValues(params: Any, message: UserScriptMessage) -> Encodable? {
63-
AIChatScriptUserValues(isAIChatHandoffEnabled: featureFlagger.isFeatureOn(.aiChatDeepLink),
64-
platform: "iOS",
70+
public func getAIChatNativeConfigValues(params: Any, message: UserScriptMessage) -> Encodable? {
71+
AIChatNativeConfigValues(isAIChatHandoffEnabled: isHandoffEnabled,
72+
platform: platform)
73+
}
74+
75+
public func getAIChatNativeHandoffData(params: Any, message: UserScriptMessage) -> Encodable? {
76+
AIChatNativeHandoffData(isAIChatHandoffEnabled: isHandoffEnabled,
77+
platform: platform,
6578
aiChatPayload: payloadHandler?.consumePayload() as? AIChatPayload)
6679
}
6780

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//
2+
// AIChatUserScriptHandlerTests.swift
3+
// DuckDuckGo
4+
//
5+
// Copyright © 2025 DuckDuckGo. All rights reserved.
6+
//
7+
// Licensed under the Apache License, Version 2.0 (the "License");
8+
// you may not use this file except in compliance with the License.
9+
// You may obtain a copy of the License at
10+
//
11+
// http://www.apache.org/licenses/LICENSE-2.0
12+
//
13+
// Unless required by applicable law or agreed to in writing, software
14+
// distributed under the License is distributed on an "AS IS" BASIS,
15+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
// See the License for the specific language governing permissions and
17+
// limitations under the License.
18+
//
19+
20+
21+
import XCTest
22+
@testable import DuckDuckGo
23+
import UserScript
24+
import WebKit
25+
26+
class AIChatUserScriptHandlerTests: XCTestCase {
27+
var aiChatUserScriptHandler: AIChatUserScriptHandler!
28+
var mockFeatureFlagger: MockFeatureFlagger!
29+
var mockPayloadHandler: MockAIChatPayloadHandling!
30+
31+
override func setUp() {
32+
super.setUp()
33+
mockFeatureFlagger = MockFeatureFlagger(enabledFeatureFlags: [.aiChatDeepLink])
34+
mockPayloadHandler = MockAIChatPayloadHandling()
35+
aiChatUserScriptHandler = AIChatUserScriptHandler(featureFlagger: mockFeatureFlagger)
36+
aiChatUserScriptHandler.setPayloadHandler(mockPayloadHandler)
37+
}
38+
39+
override func tearDown() {
40+
aiChatUserScriptHandler = nil
41+
mockFeatureFlagger = nil
42+
mockPayloadHandler = nil
43+
super.tearDown()
44+
}
45+
46+
func testGetAIChatNativeConfigValues() {
47+
// Given
48+
// MockFeatureFlagger is already initialized with .aiChatDeepLink enabled
49+
50+
// When
51+
let configValues = aiChatUserScriptHandler.getAIChatNativeConfigValues(params: [], message: MockUserScriptMessage(name: "test", body: [:])) as? AIChatNativeConfigValues
52+
53+
// Then
54+
XCTAssertNotNil(configValues)
55+
XCTAssertEqual(configValues?.isAIChatHandoffEnabled, true)
56+
XCTAssertEqual(configValues?.platform, "ios")
57+
}
58+
59+
func testGetAIChatNativeHandoffData() {
60+
// Given
61+
let expectedPayload = ["key": "value"]
62+
mockPayloadHandler.payload = expectedPayload
63+
64+
// When
65+
let handoffData = aiChatUserScriptHandler.getAIChatNativeHandoffData(params: [], message: MockUserScriptMessage(name: "test", body: [:])) as? AIChatNativeHandoffData
66+
67+
// Then
68+
XCTAssertNotNil(handoffData)
69+
XCTAssertEqual(handoffData?.isAIChatHandoffEnabled, true)
70+
XCTAssertEqual(handoffData?.platform, "ios")
71+
XCTAssertEqual(handoffData?.aiChatPayload as? [String: String], expectedPayload)
72+
}
73+
74+
func testOpenAIChat() async {
75+
// Given
76+
let expectation = self.expectation(description: "Notification should be posted")
77+
let payload = ["key": "value"]
78+
let message = MockUserScriptMessage(name: "test", body: payload)
79+
80+
// When
81+
let result = await aiChatUserScriptHandler.openAIChat(params: payload, message: message)
82+
83+
// Then
84+
XCTAssertNil(result)
85+
// Wait for the notification to be posted
86+
DispatchQueue.main.async {
87+
expectation.fulfill()
88+
}
89+
await fulfillment(of: [expectation])
90+
}
91+
}
92+
93+
class MockAIChatPayloadHandling: AIChatPayloadHandling {
94+
typealias PayloadType = [String: Any]
95+
96+
var payload: PayloadType?
97+
98+
func setPayload(_ payload: PayloadType) {
99+
self.payload = payload
100+
}
101+
102+
func consumePayload() -> PayloadType? {
103+
defer { payload = nil } // Reset the payload after consuming
104+
return payload
105+
}
106+
107+
func reset() {
108+
payload = nil
109+
}
110+
}
111+
112+
struct MockUserScriptMessage: UserScriptMessage {
113+
public var messageName: String
114+
public var messageBody: Any
115+
public var messageHost: String
116+
public var isMainFrame: Bool
117+
public var messageWebView: WKWebView?
118+
119+
// Initializer for the mock
120+
public init(messageName: String, messageBody: Any, messageHost: String, isMainFrame: Bool, messageWebView: WKWebView?) {
121+
self.messageName = messageName
122+
self.messageBody = messageBody
123+
self.messageHost = messageHost
124+
self.isMainFrame = isMainFrame
125+
self.messageWebView = messageWebView
126+
}
127+
128+
// Convenience initializer
129+
public init(name: String, body: Any) {
130+
self.messageName = name
131+
self.messageBody = body
132+
self.messageHost = "localhost" // Default value
133+
self.isMainFrame = true // Default value
134+
self.messageWebView = nil // Default value
135+
}
136+
}

0 commit comments

Comments
 (0)