-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathDebugMenuViewController.swift
More file actions
353 lines (318 loc) · 16.5 KB
/
Copy pathDebugMenuViewController.swift
File metadata and controls
353 lines (318 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
import UIKit
import WordPressShared
import AsyncImageKit
import AutomatticTracks
import BuildSettingsKit
import SwiftUI
import WordPressData
import WordPressFlux
import TipKit
import PulseUI
import Support
struct DebugMenuView: View {
@StateObject private var viewModel = DebugMenuViewModel()
@State private var isShowingWebViewURLDialog = false
@State private var webViewURL = ""
@State private var isAuthenticatedMode = true
@State private var imageCacheSize: String = ""
fileprivate var navigation: NavigationContext
var body: some View {
List {
Section { main }
Section(Strings.sectionSettings) { settings }
Section(Strings.sectionTipKit) { tipKit }
Section(Strings.sectionWebView) { webView }
Section(Strings.sectionLogging) { logging }
Section(Strings.sectionCaches) { caches }
}
.toolbar {
ToolbarItem(placement: .principal) {
(Text(Image(systemName: "bolt.fill")).foregroundColor(.yellow) + Text(" " + Strings.title)).font(.headline)
}
}
.alert(Strings.webViewDialogTitle, isPresented: $isShowingWebViewURLDialog) {
TextField("https://example.com", text: $webViewURL)
.keyboardType(.URL)
.textContentType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Button(SharedStrings.Button.cancel, role: .cancel) {
}
Button(SharedStrings.Button.view) {
if isAuthenticatedMode {
presentAuthenticatedWebView()
} else {
presentUnauthenticatedWebView()
}
}
}
}
@ViewBuilder private var main: some View {
NavigationLink {
DebugFeatureFlagsView()
} label: {
DebugMenuRow(systemImage: "flag.fill", color: .pink, title: Strings.featureFlags)
}
if ExtensiveLogging.enabled {
NavigationLink {
PulseUI.ConsoleView()
.closeButtonHidden()
} label: {
DebugMenuRow(systemImage: "message.fill", color: .green, title: Strings.logs)
}
}
NavigationLink() {
BooleanUserDefaultsDebugView()
} label: {
DebugMenuRow(systemImage: "server.rack", color: .gray, title: Strings.booleanUserDefaults)
}
}
@ViewBuilder private var settings: some View {
NavigationLink(Strings.remoteConfigTitle) {
RemoteConfigDebugView()
}
NavigationLink(Strings.sandboxStoreCookieSecretRow) {
StoreSandboxSecretScreen(cookieJar: HTTPCookieStorage.shared)
}
NavigationLink(Strings.weeklyRoundup) {
WeeklyRoundupDebugScreen()
}
NavigationLink(Strings.readerCssTitle) {
readerSettings
}
Button(Strings.showReaderInterests) {
presentReaderInterests()
}
Button(Strings.resetReaderInterestsPrompt) {
UserDefaults.standard.readerDidSelectInterestsKey = false
// A bare emoji notice is illegible on the notice's inverted
// background in dark mode; a text title renders legibly in both.
let notice = Notice(title: Strings.readerInterestsPromptResetNotice, feedbackType: .success)
ActionDispatcher.dispatch(NoticeAction.post(notice))
}
}
@ViewBuilder private var tipKit: some View {
Button(Strings.showAllTips) {
Tips.showAllTipsForTesting()
showSuccessNotice()
}
Button(Strings.hideAllTips) {
Tips.hideAllTipsForTesting()
showSuccessNotice()
}
Button(Strings.resetTipKitData, role: .destructive) {
try? Tips.resetDatastore()
showSuccessNotice()
}
}
@ViewBuilder private var webView: some View {
Button(Strings.webViewRow) {
webViewURL = Blog.lastUsed(in: ContextManager.shared.mainContext)?.url ?? ""
isAuthenticatedMode = true
isShowingWebViewURLDialog = true
}
Button(Strings.unauthenticatedWebViewRow) {
webViewURL = ""
isAuthenticatedMode = false
isShowingWebViewURLDialog = true
}
}
@ViewBuilder private var logging: some View {
Button(Strings.sendLogMessage) {
WordPressAppDelegate.crashLogging?.logMessage("Debug Log Message \(UUID().uuidString)")
showSuccessNotice()
}
Button(Strings.sendTestCrash) {
DDLogInfo("Initiating user-requested crash")
WordPressAppDelegate.crashLogging?.crash()
}
if let eventLogging = WordPressAppDelegate.eventLogging {
let viewController = EncryptedLogTableViewController(eventLogging: eventLogging)
Button {
navigation.push(viewController)
} label: {
HStack {
Text(Strings.encryptedLogging)
Spacer()
Image(systemName: "chevron.forward")
.font(.subheadline.weight(.semibold))
.foregroundStyle(.secondary.opacity(0.5))
}
.contentShape(Rectangle())
}.buttonStyle(.plain)
}
Toggle(Strings.alwaysSendLogs, isOn: $viewModel.isForcedCrashLoggingEnabled)
}
@ViewBuilder private var caches: some View {
HStack {
Text(Strings.imageCache)
Spacer()
Text(imageCacheSize)
.foregroundStyle(.secondary)
Button {
Task {
await ImageDownloader.shared.clearURLSessionCache()
await ImageDownloader.shared.clearMemoryCache()
await refreshImageCacheSize()
showSuccessNotice()
}
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
}
.task { await refreshImageCacheSize() }
}
private func refreshImageCacheSize() async {
let bytes = await ImageDownloader.shared.diskCacheSize
imageCacheSize = ByteCountFormatter.string(fromByteCount: Int64(bytes), countStyle: .file)
}
private func presentAuthenticatedWebView() {
guard let url = URL(string: webViewURL) else {
preconditionFailure("Invalid URL")
}
guard let currentBlog = Blog.lastUsed(in: ContextManager.shared.mainContext) else {
preconditionFailure("Can't get current blog")
}
let webViewController = WebViewControllerFactory.controller(
url: url,
blog: currentBlog,
source: "debug_menu"
)
navigation.push(webViewController)
}
private func presentUnauthenticatedWebView() {
guard let url = URL(string: webViewURL) else {
preconditionFailure("Invalid URL")
}
let webViewController = WebViewControllerFactory.controller(url: url, source: "debug_menu")
navigation.push(webViewController)
}
/// Presents the Reader "Discover" select-interests screen directly, bypassing
/// the `readerDidSelectInterestsKey` flag *and* the `isFollowingInterests`
/// check that normally gate it, so it can be inspected on any account.
private func presentReaderInterests() {
let interestsViewController = ReaderSelectInterestsViewController(configuration: .discover)
interestsViewController.didSaveInterests = { [weak interestsViewController] _ in
interestsViewController?.presentingViewController?.dismiss(animated: true)
}
navigation.parentViewController?.present(interestsViewController, animated: true)
}
private var readerSettings: some View {
let viewController = SettingsTextViewController(text: ReaderCSS().customAddress, placeholder: Strings.readerURLPlaceholder, hint: Strings.readerURLHint)
viewController.title = Strings.readerCssTitle
viewController.onAttributedValueChanged = {
var reader = ReaderCSS()
reader.customAddress = $0.string
}
return Wrapped(viewController: viewController)
}
}
private func showSuccessNotice() {
let notice = Notice(title: "✅", feedbackType: .success)
ActionDispatcher.dispatch(NoticeAction.post(notice))
}
private final class DebugMenuViewModel: ObservableObject {
var isForcedCrashLoggingEnabled: Bool {
get { UserSettings.userHasForcedCrashLoggingEnabled }
set {
UserSettings.userHasForcedCrashLoggingEnabled = newValue
objectWillChange.send()
}
}
}
private struct DebugMenuRow: View {
let systemImage: String
let color: Color
let title: String
var body: some View {
HStack {
Image(systemName: systemImage)
.font(.system(size: 14))
.foregroundStyle(.white)
.frame(width: 26, height: 26, alignment: .center)
.background(color)
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
Text(title)
Spacer()
}
}
}
final class DebugMenuViewController: UIHostingController<DebugMenuView> {
init() {
let navigation = NavigationContext()
super.init(rootView: DebugMenuView(navigation: navigation))
navigation.parentViewController = self
}
@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func configure(in window: UIWindow?) {
guard BuildConfiguration.current.isInternal else {
return
}
assert(window != nil)
let gesture = UIScreenEdgePanGestureRecognizer(target: DebugMenuViewController.self, action: #selector(showDebugMenu(_:)))
gesture.edges = .right
window?.addGestureRecognizer(gesture)
}
@objc private static func showDebugMenu(_ sender: UIScreenEdgePanGestureRecognizer) {
guard let window = sender.view as? UIWindow,
let topViewController = window.topmostPresentedViewController,
!((topViewController as? UINavigationController)?.viewControllers.first is DebugMenuViewController) else {
return
}
let viewController = DebugMenuViewController()
viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: .init { [weak topViewController] _ in
topViewController?.dismiss(animated: true)
})
let navigation = UINavigationController(rootViewController: viewController)
topViewController.present(navigation, animated: true)
}
}
private final class NavigationContext {
weak var parentViewController: UIViewController?
/// The alternative solution of using `UIViewControllerRepresentable` won't
/// work well without a convoluted way to pass navigaiton items.
func push(_ viewController: UIViewController) {
parentViewController?.navigationController?.pushViewController(viewController, animated: true)
}
}
private struct Wrapped<T: UIViewController>: UIViewControllerRepresentable {
let viewController: T
func makeUIViewController(context: Context) -> T { viewController }
func updateUIViewController(_ viewController: T, context: Context) {}
}
private enum Strings {
static let title = NSLocalizedString("debugMenu.title", value: "Developer", comment: "Title for debug menu screen")
static let sectionSettings = NSLocalizedString("debugMenu.section.settings", value: "Settings", comment: "Debug Menu section title")
static let sectionLogging = NSLocalizedString("debugMenu.section.logging", value: "Logging", comment: "Debug Menu section title")
static let sectionTipKit = NSLocalizedString("debugMenu.section.tipKit", value: "TipKit", comment: "Debug Menu section title")
static let sectionWebView = NSLocalizedString("debugMenu.section.webView", value: "Web View", comment: "Debug Menu section title")
static let sandboxStoreCookieSecretRow = NSLocalizedString("Sandbox Store", comment: "Title of a row displayed on the debug screen used to configure the sandbox store use in the App.")
static let sendTestCrash = NSLocalizedString("Send Test Crash", comment: "Title of a row displayed on the debug screen used to crash the app and send a crash report to the crash logging provider to ensure everything is working correctly")
static let sendLogMessage = NSLocalizedString("Send Log Message", comment: "Title of a row displayed on the debug screen used to send a pretend error message to the crash logging provider to ensure everything is working correctly")
static let alwaysSendLogs = NSLocalizedString("Always Send Crash Logs", comment: "Title of a row displayed on the debug screen used to indicate whether crash logs should be forced to send, even if they otherwise wouldn't")
static let encryptedLogging = NSLocalizedString("Encrypted Logs", comment: "Title of a row displayed on the debug screen used to display a screen that shows a list of encrypted logs")
static let readerCssTitle = NSLocalizedString("debugMenu.readerCellTitle", value: "Reader CSS URL", comment: "Title of the screen that allows the user to change the Reader CSS URL for debug builds")
static let readerURLPlaceholder = NSLocalizedString("debugMenu.readerDefaultURL", value: "Default URL", comment: "Placeholder for the reader CSS URL")
static let readerURLHint = NSLocalizedString("debugMenu.readerHit", value: "Add a custom CSS URL here to be loaded in Reader. If you're running Calypso locally this can be something like: http://192.168.15.23:3000/calypso/reader-mobile.css", comment: "Hint for the reader CSS URL field")
static let showReaderInterests = NSLocalizedString("debugMenu.showReaderInterests", value: "Show Reader Interests Screen", comment: "Debug menu action that presents the Reader select interests screen")
static let resetReaderInterestsPrompt = NSLocalizedString("debugMenu.resetReaderInterestsPrompt", value: "Reset Reader Interests Prompt", comment: "Debug menu action that clears the flag so the Reader select interests prompt can appear again")
static let readerInterestsPromptResetNotice = NSLocalizedString("debugMenu.readerInterestsPromptReset", value: "Reader interests prompt reset", comment: "Debug menu confirmation shown after clearing the flag that suppresses the Reader select interests prompt")
static let remoteConfigTitle = NSLocalizedString("debugMenu.remoteConfig.title", value: "Remote Config", comment: "Remote Config debug menu title")
static let analyics = NSLocalizedString("debugMenu.analytics", value: "Analytics", comment: "Debug menu item title")
static let featureFlags = NSLocalizedString("debugMenu.featureFlags", value: "Feature Flags", comment: "Feature flags menu item")
static let weeklyRoundup = NSLocalizedString("debugMenu.weeklyRoundup", value: "Weekly Roundup", comment: "Weekly Roundup debug menu item")
static let booleanUserDefaults = NSLocalizedString("debugMenu.booleanUserDefaults", value: "Boolean User Defaults", comment: "Boolean User Defaults debug menu item")
static let webViewRow = NSLocalizedString("debugMenu.webView.row", value: "Browse as loggedin account", comment: "Debug menu item to present an authenticated web view for the currently displayed site")
static let unauthenticatedWebViewRow = NSLocalizedString("debugMenu.webView.unauthenticatedRow", value: "Open a web browser", comment: "Debug menu item to present an unauthenticated web view")
static let webViewDialogTitle = NSLocalizedString("debugMenu.webView.dialogTitle", value: "Enter URL", comment: "Title for web view URL input dialog")
static let logs = NSLocalizedString("debugMenu.item.logs", value: "Logs", comment: "Debug Menu item title")
static let showAllTips = NSLocalizedString("debugMenu.showAllTips", value: "Show All Tips", comment: "Debug Menu action for TipKit")
static let hideAllTips = NSLocalizedString("debugMenu.hideAllTips", value: "Hide All Tips", comment: "Debug Menu action for TipKit")
static let resetTipKitData = NSLocalizedString("debugMenu.resetTipKitData", value: "Reset Data", comment: "Debug Menu action for TipKit")
static let sectionCaches = NSLocalizedString("debugMenu.section.caches", value: "Caches", comment: "Debug Menu section title")
static let imageCache = NSLocalizedString("debugMenu.caches.imageCache", value: "Image Cache", comment: "Debug Menu label for image cache size")
}