Skip to content

Commit

Permalink
Automatically clear data upon quitting (#2600)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1177771139624306/1205062321200340/f

**Description**:
Adding Burn on Quit feature for macOS. Upon standard exit, data are
cleared and the fire animation is presented. If macOS is restarting, the
app delays the restart until all data have been cleared. In the event of
an unexpected termination, such as a crash or force quit, data clearing
is handled at the next startup.
  • Loading branch information
tomasstrba authored Apr 28, 2024
1 parent 92592bb commit 36aa51f
Show file tree
Hide file tree
Showing 22 changed files with 1,362 additions and 35 deletions.
6 changes: 6 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@
1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075D28F815AD00EDFBE3 /* BWCommunicator.swift */; };
1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF075E28F815AD00EDFBE3 /* BWManager.swift */; };
1DE03425298BC7F000CAB3D7 /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; };
1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; };
1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */; };
1DFAB51D2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; };
1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */; };
1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */; };
Expand Down Expand Up @@ -2821,6 +2823,7 @@
1DDF075F28F815AD00EDFBE3 /* BWStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWStatus.swift; sourceTree = "<group>"; };
1DDF076028F815AD00EDFBE3 /* BWError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWError.swift; sourceTree = "<group>"; };
1DDF076128F815AD00EDFBE3 /* BWResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BWResponse.swift; sourceTree = "<group>"; };
1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoClearHandler.swift; sourceTree = "<group>"; };
1DFAB51C2A8982A600A0F7F6 /* SetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtension.swift; sourceTree = "<group>"; };
1DFAB51F2A89830D00A0F7F6 /* SetExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetExtensionTests.swift; sourceTree = "<group>"; };
1E0C72052ABC63BD00802009 /* SubscriptionPagesUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionPagesUserScript.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6404,6 +6407,7 @@
858A798226A8B75F00A75A42 /* CopyHandler.swift */,
1D36E65A298ACD2900AA485D /* AppIconChanger.swift */,
CB24F70B29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift */,
1DEF3BAC2BD145A9004A2FBA /* AutoClearHandler.swift */,
);
path = Application;
sourceTree = "<group>";
Expand Down Expand Up @@ -10131,6 +10135,7 @@
3706FC96293F65D500E42796 /* HorizontallyCenteredLayout.swift in Sources */,
3706FC97293F65D500E42796 /* BookmarksOutlineView.swift in Sources */,
3706FC98293F65D500E42796 /* CountryList.swift in Sources */,
1DEF3BAE2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */,
4B37EE732B4CFF0800A89A61 /* HomePageRemoteMessagingStorage.swift in Sources */,
3706FC99293F65D500E42796 /* PreferencesSection.swift in Sources */,
B6C8CAA82AD010DD0060E1CD /* YandexDataImporter.swift in Sources */,
Expand Down Expand Up @@ -10952,6 +10957,7 @@
37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */,
4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */,
CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */,
1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */,
37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */,
EEC4A66D2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */,
3776582F27F82E62009A6B35 /* AutofillPreferences.swift in Sources */,
Expand Down
19 changes: 19 additions & 0 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let internalUserDecider: InternalUserDecider
let featureFlagger: FeatureFlagger
private var appIconChanger: AppIconChanger!
private var autoClearHandler: AutoClearHandler!

private(set) var syncDataProviders: SyncDataProviders!
private(set) var syncService: DDGSyncing?
Expand Down Expand Up @@ -315,6 +316,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
#if DBP
DataBrokerProtectionAppEvents().applicationDidFinishLaunching()
#endif

setUpAutoClearHandler()
}

func applicationDidBecomeActive(_ notification: Notification) {
Expand Down Expand Up @@ -352,6 +355,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
stateRestorationManager?.applicationWillTerminate()

// Handling of "Burn on quit"
if let terminationReply = autoClearHandler.handleAppTermination() {
return terminationReply
}

return .terminateNow
}

Expand Down Expand Up @@ -550,6 +558,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
PixelKit.fire(GeneralPixel.importDataInitial, frequency: .legacyInitial)
}
}

private func setUpAutoClearHandler() {
autoClearHandler = AutoClearHandler(preferences: .shared,
fireViewModel: FireCoordinator.fireViewModel,
stateRestorationManager: stateRestorationManager)
autoClearHandler.handleAppLaunch()
autoClearHandler.onAutoClearCompleted = {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
}
}

}

func updateSubscriptionStatus() {
Expand Down
125 changes: 125 additions & 0 deletions DuckDuckGo/Application/AutoClearHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//
// AutoClearHandler.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Combine

final class AutoClearHandler {

private let preferences: DataClearingPreferences
private let fireViewModel: FireViewModel
private let stateRestorationManager: AppStateRestorationManager

init(preferences: DataClearingPreferences,
fireViewModel: FireViewModel,
stateRestorationManager: AppStateRestorationManager) {
self.preferences = preferences
self.fireViewModel = fireViewModel
self.stateRestorationManager = stateRestorationManager
}

@MainActor
func handleAppLaunch() {
burnOnStartIfNeeded()
restoreTabsIfNeeded()
resetTheCorrectTerminationFlag()
}

var onAutoClearCompleted: (() -> Void)?

@MainActor
func handleAppTermination() -> NSApplication.TerminateReply? {
guard preferences.isAutoClearEnabled else { return nil }

if preferences.isWarnBeforeClearingEnabled {
switch confirmAutoClear() {
case .alertFirstButtonReturn:
// Clear and Quit
performAutoClear()
return .terminateLater
case .alertSecondButtonReturn:
// Quit without Clearing Data
appTerminationHandledCorrectly = true
restoreTabsOnStartup = true
return .terminateNow
default:
// Cancel
return .terminateCancel
}
}

performAutoClear()
return .terminateLater
}

func resetTheCorrectTerminationFlag() {
appTerminationHandledCorrectly = false
}

// MARK: - Private

private func confirmAutoClear() -> NSApplication.ModalResponse {
let alert = NSAlert.autoClearAlert()
let response = alert.runModal()
return response
}

@MainActor
private func performAutoClear() {
fireViewModel.fire.burnAll { [weak self] in
self?.appTerminationHandledCorrectly = true
self?.onAutoClearCompleted?()
}
}

// MARK: - Burn On Start
// Burning on quit wasn't successful

@UserDefaultsWrapper(key: .appTerminationHandledCorrectly, defaultValue: false)
private var appTerminationHandledCorrectly: Bool

@MainActor
@discardableResult
func burnOnStartIfNeeded() -> Bool {
let shouldBurnOnStart = preferences.isAutoClearEnabled && !appTerminationHandledCorrectly
guard shouldBurnOnStart else { return false }

fireViewModel.fire.burnAll()
return true
}

// MARK: - Burn without Clearing Data

@UserDefaultsWrapper(key: .restoreTabsOnStartup, defaultValue: false)
private var restoreTabsOnStartup: Bool

@MainActor
@discardableResult
func restoreTabsIfNeeded() -> Bool {
let isAutoClearEnabled = preferences.isAutoClearEnabled
let restoreTabsOnStartup = restoreTabsOnStartup
self.restoreTabsOnStartup = false
if isAutoClearEnabled && restoreTabsOnStartup {
stateRestorationManager.restoreLastSessionState(interactive: false)
return true
}

return false
}

}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "Burn-Original-large.pdf",
"filename" : "Fire-96x96.pdf",
"idiom" : "universal"
}
],
Expand Down
Binary file not shown.
31 changes: 31 additions & 0 deletions DuckDuckGo/Common/Extensions/NSAlertExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,37 @@ extension NSAlert {
return alert
}

static func autoClearAlert() -> NSAlert {
let alert = NSAlert()
alert.messageText = UserText.warnBeforeQuitDialogHeader
alert.alertStyle = .warning
alert.icon = .burnAlert
alert.addButton(withTitle: UserText.clearAndQuit)
alert.addButton(withTitle: UserText.quitWithoutClearing)
alert.addButton(withTitle: UserText.cancel)

let checkbox = NSButton(checkboxWithTitle: UserText.warnBeforeQuitDialogCheckboxMessage,
target: DataClearingPreferences.shared,
action: #selector(DataClearingPreferences.toggleWarnBeforeClearing))
checkbox.state = DataClearingPreferences.shared.isWarnBeforeClearingEnabled ? .on : .off
checkbox.lineBreakMode = .byWordWrapping
checkbox.translatesAutoresizingMaskIntoConstraints = false

// Create a container view for the checkbox with custom padding
let containerView = NSView(frame: NSRect(x: 0, y: 0, width: 240, height: 25))
containerView.addSubview(checkbox)

NSLayoutConstraint.activate([
checkbox.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
checkbox.centerYAnchor.constraint(equalTo: containerView.centerYAnchor, constant: -10), // Slightly up for better visual alignment
checkbox.widthAnchor.constraint(lessThanOrEqualTo: containerView.widthAnchor)
])

alert.accessoryView = containerView

return alert
}

@discardableResult
func runModal() async -> NSApplication.ModalResponse {
await withCheckedContinuation { continuation in
Expand Down
13 changes: 13 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ struct UserText {
static let pasteAndGo = NSLocalizedString("paste.and.go", value: "Paste & Go", comment: "Paste & Go button")
static let pasteAndSearch = NSLocalizedString("paste.and.search", value: "Paste & Search", comment: "Paste & Search button")
static let clear = NSLocalizedString("clear", value: "Clear", comment: "Clear button")
static let clearAndQuit = NSLocalizedString("clear.and.quit", value: "Clear and Quit", comment: "Button to clear data and quit the application")
static let quitWithoutClearing = NSLocalizedString("quit.without.clearing", value: "Quit Without Clearing", comment: "Button to quit the application without clearing data")
static let `continue` = NSLocalizedString("`continue`", value: "Continue", comment: "Continue button")
static let bookmarkDialogAdd = NSLocalizedString("bookmark.dialog.add", value: "Add", comment: "Button to confim a bookmark creation")
static let newFolderDialogAdd = NSLocalizedString("folder.dialog.add", value: "Add", comment: "Button to confim a bookmark folder creation")
Expand Down Expand Up @@ -1077,6 +1079,17 @@ struct UserText {
static let fireproofCheckboxTitle = NSLocalizedString("fireproof.checkbox.title", value: "Ask to Fireproof websites when signing in", comment: "Fireproof settings checkbox title")
static let fireproofExplanation = NSLocalizedString("fireproof.explanation", value: "When you Fireproof a site, cookies won't be erased and you'll stay signed in, even after using the Fire Button.", comment: "Fireproofing mechanism explanation")
static let manageFireproofSites = NSLocalizedString("fireproof.manage-sites", value: "Manage Fireproof Sites…", comment: "Fireproof settings button caption")
static let autoClear = NSLocalizedString("auto.clear", value: "Auto-Clear", comment: "Header of a section in Settings. The setting configures clearing data automatically after quitting the app.")
static let automaticallyClearData = NSLocalizedString("automatically.clear.data", value: "Automatically clear tabs and browsing data when DuckDuckGo quits", comment: "Label after the checkbox in Settings which configures clearing data automatically after quitting the app.")
static let warnBeforeQuit = NSLocalizedString("warn.before.quit", value: "Warn me that tabs and data will be cleared when quitting", comment: "Label after the checkbox in Settings which configures a warning before clearing data on the application termination.")
static let warnBeforeQuitDialogHeader = NSLocalizedString("warn.before.quit.dialog.header", value: "Clear tabs and browsing data and quit DuckDuckGo?", comment: "A header of warning before clearing data on the application termination.")
static let warnBeforeQuitDialogCheckboxMessage = NSLocalizedString("warn.before.quit.dialog.checkbox.message", value: "Warn me every time", comment: "A label after checkbox to configure the warning before clearing data on the application termination.")
static let disableAutoClearToEnableSessionRestore = NSLocalizedString("disable.auto.clear.to.enable.session.restore",
value: "Disable auto-clear on quit to turn on session restore.",
comment: "Information label in Settings. It tells user that to enable session restoration setting they have to disable burn on quit. Auto-Clear should match the string with 'auto.clear' key")
static let showDataClearingSettings = NSLocalizedString("show.data.clearing.settings",
value: "Open Data Clearing Settings",
comment: "Button in Settings. It navigates user to Data Clearing Settings. The Data Clearing string should match the string with the preferences.data-clearing key")

// MARK: Crash Report
static let crashReportTitle = NSLocalizedString("crash-report.title", value: "DuckDuckGo Privacy Browser quit unexpectedly.", comment: "Title of the dialog where the user can send a crash report")
Expand Down
4 changes: 4 additions & 0 deletions DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public struct UserDefaultsWrapper<T> {
case grammarCheckEnabledOnce = "grammar.check.enabled.once"

case loginDetectionEnabled = "fireproofing.login-detection-enabled"
case autoClearEnabled = "preferences.auto-clear-enabled"
case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled"
case gpcEnabled = "preferences.gpc-enabled"
case selectedDownloadLocationKey = "preferences.download-location"
case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location"
Expand All @@ -78,6 +80,8 @@ public struct UserDefaultsWrapper<T> {
case lastCrashReportCheckDate = "last.crash.report.check.date"

case fireInfoPresentedOnce = "fire.info.presented.once"
case appTerminationHandledCorrectly = "app.termination.handled.correctly"
case restoreTabsOnStartup = "restore.tabs.on.startup"

case restorePreviousSession = "preferences.startup.restore-previous-session"
case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page"
Expand Down
Loading

0 comments on commit 36aa51f

Please sign in to comment.