Skip to content

Commit 3fbead0

Browse files
issammaninbhasin2
andauthored
Add FXIOS-11179 [Dark Mode] Use darkreader for webviews (#24327)
* chore: add darkreader lib * feat: add nimbus flag * feat: pass down flag to JS * feat: move old code to LegacyNightModeHelper and add logic to pick treatment * fix: lint * fix: wrong flag value * fix: cleanup flag check * fix: typo Co-authored-by: Nishant Bhasin <nish.bhasin@gmail.com> * fix: remove vim modeline Co-authored-by: Nishant Bhasin <nish.bhasin@gmail.com> * chore: add license * chore: move night mode to separate dir * feat: isolate nightmode into its own content world * fix: update mocks * fix: downgrade darkreader to previous version --------- Co-authored-by: Nishant Bhasin <nish.bhasin@gmail.com>
1 parent 1d52f72 commit 3fbead0

File tree

24 files changed

+421
-159
lines changed

24 files changed

+421
-159
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ firefox-ios/Client/Assets/AllFramesAtDocumentEnd.js
100100
firefox-ios/Client/Assets/MainFrameAtDocumentStart.js
101101
firefox-ios/Client/Assets/MainFrameAtDocumentEnd.js
102102
firefox-ios/Client/Assets/AutofillAllFramesAtDocumentStart.js
103+
firefox-ios/Client/Assets/NightModeMainFrameAtDocumentEnd.js
103104
firefox-ios/Client/Assets/WebcompatAllFramesAtDocumentStart.js
104105
firefox-ios/Client/Assets/addressFormLayout.js
105106
firefox-ios/Client/Assets/AddressFormManager.js

BrowserKit/Sources/Shared/Extensions/WKWebViewExtensions.swift

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ extension WKWebView {
1515
self.evaluateJavaScript(javascript, in: nil, in: .defaultClient, completionHandler: { _ in })
1616
}
1717

18+
/// This evaluates the provided JS in the specified content world
19+
/// - Parameters:
20+
/// - javascript: String representing javascript to be evaluated
21+
/// - contentWorld: The content world in which to evaluate the script
22+
public func evaluateJavascriptInCustomContentWorld(_ javascript: String, in contentWorld: WKContentWorld) {
23+
self.evaluateJavaScript(javascript, in: nil, in: contentWorld, completionHandler: { _ in })
24+
}
25+
1826
/// This calls different WebKit evaluateJavaScript functions depending on iOS version with
1927
/// a completion that passes a tuple with optional data or an optional error
2028
/// - If iOS14 or higher, evaluates Javascript in a .defaultClient sandboxed content world
@@ -73,6 +81,10 @@ extension WKUserContentController {
7381
public func addInPageContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
7482
add(scriptMessageHandler, contentWorld: .page, name: name)
7583
}
84+
85+
public func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
86+
add(scriptMessageHandler, contentWorld: .world(name: name), name: name)
87+
}
7688
}
7789

7890
extension WKUserScript {

BrowserKit/Sources/WebEngine/WKWebview/Scripts/WKContentScriptManager.swift

+21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ protocol WKContentScriptManager: WKScriptMessageHandler {
1717
name: String,
1818
forSession session: WKEngineSession)
1919

20+
func addContentScriptToCustomWorld(_ script: WKContentScript,
21+
name: String,
22+
forSession session: WKEngineSession)
23+
2024
func uninstall(session: WKEngineSession)
2125
}
2226

@@ -57,6 +61,23 @@ class DefaultContentScriptManager: NSObject, WKContentScriptManager {
5761
}
5862
}
5963

64+
func addContentScriptToCustomWorld(_ script: WKContentScript,
65+
name: String,
66+
forSession session: WKEngineSession) {
67+
// If a script already exists on the page, skip adding this duplicate.
68+
guard scripts[name] == nil else { return }
69+
70+
scripts[name] = script
71+
72+
// If this helper handles script messages, then get the handlers names and register them
73+
script.scriptMessageHandlerNames().forEach { scriptMessageHandlerName in
74+
session.webView.engineConfiguration.addInCustomContentWorld(
75+
scriptMessageHandler: self,
76+
name: name
77+
)
78+
}
79+
}
80+
6081
func userContentController(_ userContentController: WKUserContentController,
6182
didReceive message: WKScriptMessage) {
6283
for script in scripts.values where script.scriptMessageHandlerNames().contains(message.name) {

BrowserKit/Sources/WebEngine/WKWebview/WKEngineConfiguration.swift

+7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ protocol WKEngineConfiguration {
1010
func addUserScript(_ userScript: WKUserScript)
1111
func addInDefaultContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
1212
func addInPageContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
13+
func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String)
1314
func removeScriptMessageHandler(forName name: String)
1415
func removeAllUserScripts()
1516
}
@@ -35,6 +36,12 @@ struct DefaultEngineConfiguration: WKEngineConfiguration {
3536
name: name)
3637
}
3738

39+
func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
40+
webViewConfiguration.userContentController.add(scriptMessageHandler,
41+
contentWorld: .world(name: name),
42+
name: name)
43+
}
44+
3845
func removeScriptMessageHandler(forName name: String) {
3946
webViewConfiguration.userContentController.removeScriptMessageHandler(forName: name)
4047
}

BrowserKit/Tests/WebEngineTests/Mock/MockWKContentScriptManager.swift

+10
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ class MockWKContentScriptManager: NSObject, WKContentScriptManager {
1010
var scripts = [String: WKContentScript]()
1111
var addContentScriptCalled = 0
1212
var addContentScriptToPageCalled = 0
13+
var addContentScriptToCustomWorldCalled = 0
1314
var uninstallCalled = 0
1415
var userContentControllerCalled = 0
1516

1617
var savedContentScriptNames = [String]()
1718
var savedContentScriptPageNames = [String]()
19+
var savedContentScriptCustomWorldNames = [String]()
1820

1921
func addContentScript(_ script: WKContentScript,
2022
name: String,
@@ -32,6 +34,14 @@ class MockWKContentScriptManager: NSObject, WKContentScriptManager {
3234
addContentScriptToPageCalled += 1
3335
}
3436

37+
func addContentScriptToCustomWorld(_ script: WKContentScript,
38+
name: String,
39+
forSession session: WKEngineSession) {
40+
scripts[name] = script
41+
savedContentScriptCustomWorldNames.append(name)
42+
addContentScriptToCustomWorldCalled += 1
43+
}
44+
3545
func uninstall(session: WKEngineSession) {
3646
uninstallCalled += 1
3747
}

BrowserKit/Tests/WebEngineTests/Mock/MockWKEngineConfiguration.swift

+6
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class MockWKEngineConfiguration: WKEngineConfiguration {
1111
var addUserScriptCalled = 0
1212
var addInDefaultContentWorldCalled = 0
1313
var addInPageContentWorldCalled = 0
14+
var addInCustomContentWorldCalled = 0
1415
var removeScriptMessageHandlerCalled = 0
1516
var removeAllUserScriptsCalled = 0
1617

@@ -28,6 +29,11 @@ class MockWKEngineConfiguration: WKEngineConfiguration {
2829
addInPageContentWorldCalled += 1
2930
}
3031

32+
func addInCustomContentWorld(scriptMessageHandler: WKScriptMessageHandler, name: String) {
33+
scriptNameAdded = name
34+
addInCustomContentWorldCalled += 1
35+
}
36+
3137
func removeScriptMessageHandler(forName name: String) {
3238
removeScriptMessageHandlerCalled += 1
3339
}

firefox-ios/Client.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,7 @@
12741274
BA1C68BC2B7ED153000D9397 /* MockWebKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1C68BB2B7ED153000D9397 /* MockWebKit.swift */; };
12751275
BA7A14842C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7A14832C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift */; };
12761276
BA8E197F2BF2FB1900590B5F /* AddressFormManager.js in Resources */ = {isa = PBXBuildFile; fileRef = BA8E197E2BF2FB1900590B5F /* AddressFormManager.js */; };
1277+
BA9AB4942D49A55900DD85E0 /* NightModeMainFrameAtDocumentEnd.js in Resources */ = {isa = PBXBuildFile; fileRef = BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */; };
12771278
BC003F5E2B59F44600929ECB /* BrowserViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC003F5D2B59F44500929ECB /* BrowserViewControllerTests.swift */; };
12781279
BCFF93EE2AAA9F6E005B5B71 /* RustFirefoxSuggest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF93ED2AAA9C47005B5B71 /* RustFirefoxSuggest.swift */; };
12791280
BCFF93F02AABA55A005B5B71 /* BackgroundFirefoxSuggestIngestUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFF93EF2AAB97A8005B5B71 /* BackgroundFirefoxSuggestIngestUtility.swift */; };
@@ -8494,6 +8495,7 @@
84948495
BA7A14832C2CCEB3008DF1D9 /* EditAddressWebViewManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAddressWebViewManager.swift; sourceTree = "<group>"; };
84958496
BA8E197E2BF2FB1900590B5F /* AddressFormManager.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = AddressFormManager.js; sourceTree = "<group>"; };
84968497
BA904A3B89BC820A7A802D55 /* es-CL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-CL"; path = "es-CL.lproj/Storage.strings"; sourceTree = "<group>"; };
8498+
BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = NightModeMainFrameAtDocumentEnd.js; path = Client/Assets/NightModeMainFrameAtDocumentEnd.js; sourceTree = "<group>"; };
84978499
BAA64356B54F1CD258764620 /* su */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = su; path = su.lproj/FindInPage.strings; sourceTree = "<group>"; };
84988500
BAF14FEC94CB9DA4E08BA60C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Storage.strings; sourceTree = "<group>"; };
84998501
BAF74394B38CDAE36910B788 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Shared.strings; sourceTree = "<group>"; };
@@ -14373,6 +14375,7 @@
1437314375
F84B21B51A090F8100AAB793 = {
1437414376
isa = PBXGroup;
1437514377
children = (
14378+
BA9AB4932D49A55700DD85E0 /* NightModeMainFrameAtDocumentEnd.js */,
1437614379
2FA435FC1ABB83B4008031D1 /* Account */,
1437714380
D4F0C2642B2C90FB008ECEE8 /* BrowserKit */,
1437814381
F84B21C01A090F8100AAB793 /* Client */,
@@ -15697,6 +15700,7 @@
1569715700
isa = PBXResourcesBuildPhase;
1569815701
buildActionMask = 2147483647;
1569915702
files = (
15703+
BA9AB4942D49A55900DD85E0 /* NightModeMainFrameAtDocumentEnd.js in Resources */,
1570015704
BA8E197F2BF2FB1900590B5F /* AddressFormManager.js in Resources */,
1570115705
8A1A935B2B757C7C0069C190 /* wave.json in Resources */,
1570215706
D4AFAB0E2AFA8F9A000BFEAA /* SyncIntegrationTests in Resources */,

firefox-ios/Client/Assets/About/Licenses.html

+34
Original file line numberDiff line numberDiff line change
@@ -747,5 +747,39 @@ <h4 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “
747747
</div>
748748
<center><p>-</p></center>
749749

750+
<div>
751+
<input id="ac-28" name="accordion-1" type="checkbox" />
752+
<label for="ac-28"><h2>darkreader</h2></label>
753+
<article>
754+
<p class="link"><a href="https://github.com/darkreader/darkreader">https://github.com/darkreader/darkreader</a></p>
755+
<div class="text">
756+
<p>MIT License</p>
757+
758+
<p>Copyright (c) 2024 Dark Reader Ltd.</p>
759+
760+
<p>All rights reserved.</p>
761+
762+
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
763+
of this software and associated documentation files (the "Software"), to deal
764+
in the Software without restriction, including without limitation the rights
765+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
766+
copies of the Software, and to permit persons to whom the Software is
767+
furnished to do so, subject to the following conditions:</p>
768+
769+
<p>The above copyright notice and this permission notice shall be included in all
770+
copies or substantial portions of the Software.</p>
771+
772+
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
773+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
774+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
775+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
776+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
777+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
778+
SOFTWARE.</p>
779+
</div>
780+
</article>
781+
</div>
782+
<center><p>-</p></center>
783+
750784
</html>
751785

firefox-ios/Client/FeatureFlags/NimbusFlaggableFeature.swift

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum NimbusFeatureFlagID: String, CaseIterable {
1616
case contextualHintForToolbar
1717
case creditCardAutofillStatus
1818
case cleanupHistoryReenabled
19+
case darkReader
1920
case fakespotBackInStock
2021
case fakespotFeature
2122
case fakespotProductAds
@@ -112,6 +113,7 @@ struct NimbusFlaggableFeature: HasNimbusSearchBar {
112113
.cleanupHistoryReenabled,
113114
.creditCardAutofillStatus,
114115
.closeRemoteTabs,
116+
.darkReader,
115117
.fakespotBackInStock,
116118
.fakespotFeature,
117119
.fakespotProductAds,

firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -3566,7 +3566,7 @@ extension BrowserViewController: LegacyTabDelegate {
35663566
tab.addContentScriptToPage(printHelper, name: PrintHelper.name())
35673567

35683568
let nightModeHelper = NightModeHelper()
3569-
tab.addContentScript(nightModeHelper, name: NightModeHelper.name())
3569+
tab.addContentScriptToCustomWorld(nightModeHelper, name: NightModeHelper.name())
35703570

35713571
// XXX: Bug 1390200 - Disable NSUserActivity/CoreSpotlight temporarily
35723572
// let spotlightHelper = SpotlightHelper(tab: tab)

firefox-ios/Client/Frontend/TabContentsScripts/NightModeHelper.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ class NightModeHelper: TabContentScript, FeatureFlaggable {
2121
return ["NightMode"]
2222
}
2323

24+
static func jsCallbackBuilder(_ enabled: Bool) -> String {
25+
let isDarkReader = LegacyFeatureFlagsManager.shared.isFeatureEnabled(.darkReader, checking: .buildOnly)
26+
return "window.__firefox__.NightMode.setEnabled(\(enabled), \(isDarkReader))"
27+
}
28+
2429
func userContentController(
2530
_ userContentController: WKUserContentController,
2631
didReceiveScriptMessage message: WKScriptMessage
2732
) {
2833
guard let webView = message.frameInfo.webView else { return }
29-
let jsCallback = "window.__firefox__.NightMode.setEnabled(\(NightModeHelper.isActivated()))"
30-
webView.evaluateJavascriptInDefaultContentWorld(jsCallback)
34+
webView.evaluateJavascriptInCustomContentWorld(
35+
NightModeHelper.jsCallbackBuilder(NightModeHelper.isActivated()),
36+
in: .world(name: NightModeHelper.name())
37+
)
3138
}
3239

3340
static func toggle(

firefox-ios/Client/Frontend/TabContentsScripts/UserScriptManager.swift

+31-18
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ class UserScriptManager: FeatureFlaggable {
1717
source: "window.__firefox__.NoImageMode.setEnabled(true)",
1818
injectionTime: .atDocumentStart,
1919
forMainFrameOnly: true)
20-
private let nightModeUserScript = WKUserScript.createInDefaultContentWorld(
21-
source: "window.__firefox__.NightMode.setEnabled(true)",
20+
private let nightModeUserScript = WKUserScript(
21+
source: NightModeHelper.jsCallbackBuilder(true),
2222
injectionTime: .atDocumentEnd,
23-
forMainFrameOnly: true)
23+
forMainFrameOnly: true,
24+
in: .world(name: NightModeHelper.name()))
2425
private let printHelperUserScript = WKUserScript.createInPageContentWorld(
2526
source: "window.print = function () { window.webkit.messageHandlers.printHandler.postMessage({}) }",
2627
injectionTime: .atDocumentEnd,
@@ -39,8 +40,7 @@ class UserScriptManager: FeatureFlaggable {
3940
let mainframeString = mainFrameOnly ? "MainFrame" : "AllFrames"
4041
let injectionString = injectionTime == .atDocumentStart ? "Start" : "End"
4142
let name = mainframeString + "AtDocument" + injectionString
42-
if let path = Bundle.main.path(forResource: name, ofType: "js"),
43-
let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
43+
if let source = UserScriptManager.getScriptSource(name) {
4444
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
4545
let userScript = WKUserScript.createInDefaultContentWorld(
4646
source: wrappedSource,
@@ -52,11 +52,7 @@ class UserScriptManager: FeatureFlaggable {
5252

5353
// Autofill scripts
5454
let autofillName = "Autofill\(name)"
55-
if let autofillScriptCompatPath = Bundle.main.path(
56-
forResource: autofillName, ofType: "js"),
57-
let source = try? NSString(
58-
contentsOfFile: autofillScriptCompatPath,
59-
encoding: String.Encoding.utf8.rawValue) as String {
55+
if let source = UserScriptManager.getScriptSource(autofillName) {
6056
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
6157
let userScript = WKUserScript.createInDefaultContentWorld(
6258
source: wrappedSource,
@@ -65,15 +61,20 @@ class UserScriptManager: FeatureFlaggable {
6561
compiledUserScripts[autofillName] = userScript
6662
}
6763

64+
// NightMode scripts
65+
let nightModeName = "NightMode\(name)"
66+
if let source = UserScriptManager.getScriptSource(nightModeName) {
67+
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
68+
let userScript = WKUserScript(
69+
source: wrappedSource,
70+
injectionTime: injectionTime,
71+
forMainFrameOnly: mainFrameOnly,
72+
in: .world(name: NightModeHelper.name()))
73+
compiledUserScripts[nightModeName] = userScript
74+
}
75+
6876
let webcompatName = "Webcompat\(name)"
69-
if let webCompatPath = Bundle.main.path(
70-
forResource: webcompatName,
71-
ofType: "js"
72-
),
73-
let source = try? NSString(
74-
contentsOfFile: webCompatPath,
75-
encoding: String.Encoding.utf8.rawValue
76-
) as String {
77+
if let source = UserScriptManager.getScriptSource(webcompatName) {
7778
let wrappedSource = "(function() { const APP_ID_TOKEN = '\(UserScriptManager.appIdToken)'; \(source) })()"
7879
let userScript = WKUserScript.createInPageContentWorld(
7980
source: wrappedSource,
@@ -87,6 +88,13 @@ class UserScriptManager: FeatureFlaggable {
8788
self.compiledUserScripts = compiledUserScripts
8889
}
8990

91+
private static func getScriptSource(_ scriptName: String) -> String? {
92+
guard let path = Bundle.main.path(forResource: scriptName, ofType: "js") else {
93+
return nil
94+
}
95+
return try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String
96+
}
97+
9098
public func injectUserScriptsIntoWebView(_ webView: WKWebView?, nightMode: Bool, noImageMode: Bool) {
9199
// Start off by ensuring that any previously-added user scripts are
92100
// removed to prevent the same script from being injected twice.
@@ -110,6 +118,11 @@ class UserScriptManager: FeatureFlaggable {
110118
webView?.configuration.userContentController.addUserScript(autofillScript)
111119
}
112120

121+
let nightModeName = "NightMode\(name)"
122+
if let nightModeScript = compiledUserScripts[nightModeName] {
123+
webView?.configuration.userContentController.addUserScript(nightModeScript)
124+
}
125+
113126
let webcompatName = "Webcompat\(name)"
114127
if let webcompatUserScript = compiledUserScripts[webcompatName] {
115128
webView?.configuration.userContentController.addUserScript(webcompatUserScript)

0 commit comments

Comments
 (0)