Skip to content

Commit ad0dbbb

Browse files
authored
New permission authorization and new permission center (#2700)
<!-- Note: This template is a reminder of our Engineering Expectations and Definition of Done. Remove sections that don't apply to your changes. ⚠️ If you're an external contributor, please file an issue before working on a PR. Discussing your changes beforehand will help ensure they align with our roadmap and that your time is well spent. --> Task/Issue URL: https://app.asana.com/1/137249556945/project/1148564399326804/task/1212226055102691?focus=true ### Description This PR adds a new permission authorization flow and basic functionality of new permission center, which unblocks the notifications API work. Unit tests, UI tests and support of popups will be added in a follow up PR.
1 parent 5c48735 commit ad0dbbb

24 files changed

+1992
-46
lines changed

SharedPackages/BrowserServicesKit/Sources/BrowserServicesKit/PrivacyConfig/Features/PrivacyFeature.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public enum MacOSBrowserConfigSubfeature: String, PrivacySubfeature {
143143
/// Tab closing event recreation feature flag (failsafe for removing private API)
144144
/// https://app.asana.com/1/137249556945/project/1211834678943996/task/1212206087745586?focus=true
145145
case tabClosingEventRecreation
146+
146147
}
147148

148149
public enum iOSBrowserConfigSubfeature: String, PrivacySubfeature {

macOS/DuckDuckGo-macOS.xcodeproj/project.pbxproj

Lines changed: 44 additions & 0 deletions
Large diffs are not rendered by default.

macOS/DuckDuckGo/Application/AppDelegate.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -704,16 +704,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
704704
if AppVersion.runType.requiresEnvironment {
705705
fireproofDomains = FireproofDomains(store: FireproofDomainsStore(database: database.db, tableName: "FireproofDomains"), tld: tld)
706706
faviconManager = FaviconManager(cacheType: .standard(database.db), bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains)
707-
permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db))
707+
permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db), featureFlagger: featureFlagger)
708708
} else {
709709
fireproofDomains = FireproofDomains(store: FireproofDomainsStore(context: nil), tld: tld)
710710
faviconManager = FaviconManager(cacheType: .inMemory, bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains)
711-
permissionManager = PermissionManager(store: LocalPermissionStore(database: nil))
711+
permissionManager = PermissionManager(store: LocalPermissionStore(database: nil), featureFlagger: featureFlagger)
712712
}
713713
#else
714714
fireproofDomains = FireproofDomains(store: FireproofDomainsStore(database: database.db, tableName: "FireproofDomains"), tld: tld)
715715
faviconManager = FaviconManager(cacheType: .standard(database.db), bookmarkManager: bookmarkManager, fireproofDomains: fireproofDomains)
716-
permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db))
716+
permissionManager = PermissionManager(store: LocalPermissionStore(database: database.db), featureFlagger: featureFlagger)
717717
#endif
718718

719719
webCacheManager = WebCacheManager(fireproofDomains: fireproofDomains)

macOS/DuckDuckGo/Common/Extensions/NSColorExtension.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ extension NSColor {
5252
.blackWhite10
5353
}
5454

55+
/// Background color for permission warning rows (system permission disabled)
56+
/// Light mode: #FFF0C2, Dark mode: #C18010 at 16% opacity
57+
static var permissionWarningBackground: NSColor {
58+
NSColor(name: nil) { appearance in
59+
if appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
60+
return NSColor(red: 0xC1 / 255.0, green: 0x80 / 255.0, blue: 0x10 / 255.0, alpha: 0.16)
61+
} else {
62+
return NSColor(red: 0xFF / 255.0, green: 0xF0 / 255.0, blue: 0xC2 / 255.0, alpha: 1.0)
63+
}
64+
}
65+
}
66+
5567
// MARK: - Helpers
5668

5769
var ciColor: CIColor {

macOS/DuckDuckGo/Common/Localizables/UserText.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,11 +977,33 @@ struct UserText {
977977
static let permissionPopupLearnMoreLink = NSLocalizedString("permission.popup.learn-more.link", value: "Learn more about location services", comment: "Text of link that leads to web page with more informations about location services.")
978978
static let permissionPopupAllowButton = NSLocalizedString("permission.popup.allow.button", value: "Allow", comment: "Button that the user can use to authorise a web site to for, for example access location or camera and microphone etc.")
979979

980+
static let permissionPopupDenyButton = NSLocalizedString("permission.popup.deny.button", value: "Deny", comment: "Button that denies permission for this request only")
981+
static let permissionPopupAlwaysDenyButton = NSLocalizedString("permission.popup.always.deny.button", value: "Never Allow", comment: "Button that denies permission and remembers the decision for future requests")
982+
static let permissionPopupAlwaysAllowButton = NSLocalizedString("permission.popup.always.allow.button", value: "Always Allow", comment: "Button that grants permission and remembers the decision for future requests")
983+
980984
static let privacyDashboardPermissionAsk = NSLocalizedString("dashboard.permission.ask", value: "Ask every time", comment: "Privacy Dashboard: Website should always Ask for permission for input media device access")
981985
static let privacyDashboardPermissionAlwaysAllow = NSLocalizedString("dashboard.permission.allow", value: "Always allow", comment: "Privacy Dashboard: Website can always access input media device")
982986
static let privacyDashboardPermissionAlwaysDeny = NSLocalizedString("dashboard.permission.deny", value: "Always deny", comment: "Privacy Dashboard: Website can never access input media device")
983987
static let permissionPopoverDenyButton = NSLocalizedString("permission.popover.deny", value: "Deny", comment: "Permission Popover: Deny Website input media device access")
984988

989+
// Two-step permission authorization (geolocation)
990+
static let permissionSystemLocationEnable = NSLocalizedString("permission.system.location.enable", value: "Enable System Location", comment: "Button to enable system location services")
991+
static let permissionSystemLocationWaiting = NSLocalizedString("permission.system.location.waiting", value: "Waiting for system permission…", comment: "Text shown while waiting for user to respond to system location permission dialog")
992+
static let permissionSystemLocationEnabled = NSLocalizedString("permission.system.location.enabled", value: "System location enabled!", comment: "Text shown after system location permission has been granted")
993+
static let permissionSystemLocationDisabled = NSLocalizedString("permission.system.location.disabled", value: "System location disabled. Turn it on in ", comment: "Text shown when system location was previously denied. Followed by a link to System Settings")
994+
static let permissionSystemSettingsLocation = NSLocalizedString("permission.system.settings.location", value: "System Settings → Privacy", comment: "Link text to open System Settings Privacy section for location")
995+
static let permissionRestartApp = NSLocalizedString("permission.restart.app", value: "Restart the DuckDuckGo application", comment: "Text shown when app restart is required for permission changes to take effect")
996+
static let permissionGeolocationPromptFormat = NSLocalizedString("permission.geolocation.prompt.format", value: "Allow %@ to use your current location?", comment: "Prompt asking if domain %@ can use location")
997+
static let permissionPopupNeverAllowButton = NSLocalizedString("permission.popup.never.allow.button", value: "Never Allow", comment: "Button that denies permission and remembers the decision for future requests")
998+
999+
// Permission Center
1000+
static let permissionCenterTitle = NSLocalizedString("permission.center.title", value: "Permissions for \"%@\"", comment: "Title for permission center popover, %@ is the domain name")
1001+
static let permissionCenterAlwaysAsk = NSLocalizedString("permission.center.always.ask", value: "Always Ask", comment: "Permission center dropdown option to always ask for permission")
1002+
static let permissionCenterAlwaysAllow = NSLocalizedString("permission.center.always.allow", value: "Always Allow", comment: "Permission center dropdown option to always allow permission")
1003+
static let permissionCenterNeverAllow = NSLocalizedString("permission.center.never.allow", value: "Never Allow", comment: "Permission center dropdown option to never allow permission")
1004+
static let permissionCenterExternalSchemeDescription = NSLocalizedString("permission.center.external.scheme.description", value: "%@ to open \"%@\" links", comment: "Description for external scheme permission, first %@ is domain, second %@ is scheme name")
1005+
static let permissionCenterExternalApps = NSLocalizedString("permission.center.external.apps", value: "External Apps", comment: "Permission center header for external app permissions")
1006+
9851007
static let privacyDashboardPopupsAlwaysAsk = NSLocalizedString("dashboard.popups.ask", value: "Notify", comment: "Make pop-up windows always request permission for the current domain")
9861008

9871009
static let settings = NSLocalizedString("settings", value: "Settings", comment: "Menu item for opening settings")

macOS/DuckDuckGo/Localization/Localizable.xcstrings

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60390,6 +60390,78 @@
6039060390
}
6039160391
}
6039260392
},
60393+
"permission.center.always.allow" : {
60394+
"comment" : "Permission center dropdown option to always allow permission",
60395+
"extractionState" : "extracted_with_value",
60396+
"localizations" : {
60397+
"en" : {
60398+
"stringUnit" : {
60399+
"state" : "new",
60400+
"value" : "Always Allow"
60401+
}
60402+
}
60403+
}
60404+
},
60405+
"permission.center.always.ask" : {
60406+
"comment" : "Permission center dropdown option to always ask for permission",
60407+
"extractionState" : "extracted_with_value",
60408+
"localizations" : {
60409+
"en" : {
60410+
"stringUnit" : {
60411+
"state" : "new",
60412+
"value" : "Always Ask"
60413+
}
60414+
}
60415+
}
60416+
},
60417+
"permission.center.external.apps" : {
60418+
"comment" : "Permission center header for external app permissions",
60419+
"extractionState" : "extracted_with_value",
60420+
"localizations" : {
60421+
"en" : {
60422+
"stringUnit" : {
60423+
"state" : "new",
60424+
"value" : "External Apps"
60425+
}
60426+
}
60427+
}
60428+
},
60429+
"permission.center.external.scheme.description" : {
60430+
"comment" : "Description for external scheme permission, first %@ is domain, second %@ is scheme name",
60431+
"extractionState" : "extracted_with_value",
60432+
"localizations" : {
60433+
"en" : {
60434+
"stringUnit" : {
60435+
"state" : "new",
60436+
"value" : "%1$@ to open \"%2$@\" links"
60437+
}
60438+
}
60439+
}
60440+
},
60441+
"permission.center.never.allow" : {
60442+
"comment" : "Permission center dropdown option to never allow permission",
60443+
"extractionState" : "extracted_with_value",
60444+
"localizations" : {
60445+
"en" : {
60446+
"stringUnit" : {
60447+
"state" : "new",
60448+
"value" : "Never Allow"
60449+
}
60450+
}
60451+
}
60452+
},
60453+
"permission.center.title" : {
60454+
"comment" : "Title for permission center popover, %@ is the domain name",
60455+
"extractionState" : "extracted_with_value",
60456+
"localizations" : {
60457+
"en" : {
60458+
"stringUnit" : {
60459+
"state" : "new",
60460+
"value" : "Permissions for \"%@\""
60461+
}
60462+
}
60463+
}
60464+
},
6039360465
"permission.disabled.app" : {
6039460466
"comment" : "The app (DuckDuckGo: %@ 2) has no access permission to (%@ 1) media device",
6039560467
"extractionState" : "extracted_with_value",
@@ -60630,6 +60702,18 @@
6063060702
}
6063160703
}
6063260704
},
60705+
"permission.geolocation.prompt.format" : {
60706+
"comment" : "Prompt asking if domain %@ can use location",
60707+
"extractionState" : "extracted_with_value",
60708+
"localizations" : {
60709+
"en" : {
60710+
"stringUnit" : {
60711+
"state" : "new",
60712+
"value" : "Allow %@ to use your current location?"
60713+
}
60714+
}
60715+
}
60716+
},
6063360717
"permission.microphone" : {
6063460718
"comment" : "Microphone input media device name",
6063560719
"extractionState" : "extracted_with_value",
@@ -60990,6 +61074,30 @@
6099061074
}
6099161075
}
6099261076
},
61077+
"permission.popup.always.allow.button" : {
61078+
"comment" : "Button that grants permission and remembers the decision for future requests",
61079+
"extractionState" : "extracted_with_value",
61080+
"localizations" : {
61081+
"en" : {
61082+
"stringUnit" : {
61083+
"state" : "new",
61084+
"value" : "Always Allow"
61085+
}
61086+
}
61087+
}
61088+
},
61089+
"permission.popup.always.deny.button" : {
61090+
"comment" : "Button that denies permission and remembers the decision for future requests",
61091+
"extractionState" : "extracted_with_value",
61092+
"localizations" : {
61093+
"en" : {
61094+
"stringUnit" : {
61095+
"state" : "new",
61096+
"value" : "Never Allow"
61097+
}
61098+
}
61099+
}
61100+
},
6099361101
"permission.popup.blocked.popover" : {
6099461102
"comment" : "Text of popover warning the user that a pop-up has been blocked",
6099561103
"extractionState" : "extracted_with_value",
@@ -61050,6 +61158,18 @@
6105061158
}
6105161159
}
6105261160
},
61161+
"permission.popup.deny.button" : {
61162+
"comment" : "Button that denies permission for this request only",
61163+
"extractionState" : "extracted_with_value",
61164+
"localizations" : {
61165+
"en" : {
61166+
"stringUnit" : {
61167+
"state" : "new",
61168+
"value" : "Deny"
61169+
}
61170+
}
61171+
}
61172+
},
6105361173
"permission.popup.learn-more.link" : {
6105461174
"comment" : "Text of link that leads to web page with more informations about location services.",
6105561175
"extractionState" : "extracted_with_value",
@@ -61110,6 +61230,18 @@
6111061230
}
6111161231
}
6111261232
},
61233+
"permission.popup.never.allow.button" : {
61234+
"comment" : "Button that denies permission and remembers the decision for future requests",
61235+
"extractionState" : "extracted_with_value",
61236+
"localizations" : {
61237+
"en" : {
61238+
"stringUnit" : {
61239+
"state" : "new",
61240+
"value" : "Never Allow"
61241+
}
61242+
}
61243+
}
61244+
},
6111361245
"permission.popup.open.format" : {
6111461246
"comment" : "Menu action to open the blocked pop-up at the specified URL",
6111561247
"extractionState" : "extracted_with_value",
@@ -61571,6 +61703,78 @@
6157161703
}
6157261704
}
6157361705
},
61706+
"permission.restart.app" : {
61707+
"comment" : "Text shown when app restart is required for permission changes to take effect",
61708+
"extractionState" : "extracted_with_value",
61709+
"localizations" : {
61710+
"en" : {
61711+
"stringUnit" : {
61712+
"state" : "new",
61713+
"value" : "Restart the DuckDuckGo application"
61714+
}
61715+
}
61716+
}
61717+
},
61718+
"permission.system.location.disabled" : {
61719+
"comment" : "Text shown when system location was previously denied. Followed by a link to System Settings",
61720+
"extractionState" : "extracted_with_value",
61721+
"localizations" : {
61722+
"en" : {
61723+
"stringUnit" : {
61724+
"state" : "new",
61725+
"value" : "System location disabled. Turn it on in "
61726+
}
61727+
}
61728+
}
61729+
},
61730+
"permission.system.location.enable" : {
61731+
"comment" : "Button to enable system location services",
61732+
"extractionState" : "extracted_with_value",
61733+
"localizations" : {
61734+
"en" : {
61735+
"stringUnit" : {
61736+
"state" : "new",
61737+
"value" : "Enable System Location"
61738+
}
61739+
}
61740+
}
61741+
},
61742+
"permission.system.location.enabled" : {
61743+
"comment" : "Text shown after system location permission has been granted",
61744+
"extractionState" : "extracted_with_value",
61745+
"localizations" : {
61746+
"en" : {
61747+
"stringUnit" : {
61748+
"state" : "new",
61749+
"value" : "System location enabled!"
61750+
}
61751+
}
61752+
}
61753+
},
61754+
"permission.system.location.waiting" : {
61755+
"comment" : "Text shown while waiting for user to respond to system location permission dialog",
61756+
"extractionState" : "extracted_with_value",
61757+
"localizations" : {
61758+
"en" : {
61759+
"stringUnit" : {
61760+
"state" : "new",
61761+
"value" : "Waiting for system permission…"
61762+
}
61763+
}
61764+
}
61765+
},
61766+
"permission.system.settings.location" : {
61767+
"comment" : "Link text to open System Settings Privacy section for location",
61768+
"extractionState" : "extracted_with_value",
61769+
"localizations" : {
61770+
"en" : {
61771+
"stringUnit" : {
61772+
"state" : "new",
61773+
"value" : "System Settings → Privacy"
61774+
}
61775+
}
61776+
}
61777+
},
6157461778
"permission.unmute" : {
6157561779
"comment" : "Resume input media device %@ access for %@ website",
6157661780
"extractionState" : "extracted_with_value",

macOS/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ final class AddressBarButtonsViewController: NSViewController {
6161
private var permissionAuthorizationPopover: PermissionAuthorizationPopover?
6262
private func permissionAuthorizationPopoverCreatingIfNeeded() -> PermissionAuthorizationPopover {
6363
return permissionAuthorizationPopover ?? {
64-
let popover = PermissionAuthorizationPopover()
64+
let popover = PermissionAuthorizationPopover(featureFlagger: featureFlagger)
6565
NotificationCenter.default.addObserver(self, selector: #selector(popoverDidClose), name: NSPopover.didCloseNotification, object: popover)
6666
NotificationCenter.default.addObserver(self, selector: #selector(popoverWillShow), name: NSPopover.willShowNotification, object: popover)
6767
self.permissionAuthorizationPopover = popover
@@ -70,6 +70,8 @@ final class AddressBarButtonsViewController: NSViewController {
7070
}()
7171
}
7272

73+
private var permissionCenterPopover: PermissionCenterPopover?
74+
7375
private var popupBlockedPopover: PopupBlockedPopover?
7476
private func popupBlockedPopoverCreatingIfNeeded() -> PopupBlockedPopover {
7577
return popupBlockedPopover ?? {
@@ -1610,7 +1612,36 @@ final class AddressBarButtonsViewController: NSViewController {
16101612
}
16111613

16121614
@IBAction func permissionCenterButtonAction(_ sender: Any) {
1615+
guard featureFlagger.isFeatureOn(.newPermissionView) else { return }
1616+
guard let tabViewModel else { return }
1617+
1618+
// Close existing popover if shown
1619+
if let existingPopover = permissionCenterPopover, existingPopover.isShown {
1620+
existingPopover.close()
1621+
permissionCenterPopover = nil
1622+
return
1623+
}
16131624

1625+
let url = tabViewModel.tab.content.urlForWebView ?? .empty
1626+
let domain = (url.isFileURL ? .localhost : (url.host ?? "")).droppingWwwPrefix()
1627+
1628+
let viewModel = PermissionCenterViewModel(
1629+
domain: domain,
1630+
usedPermissions: tabViewModel.usedPermissions,
1631+
permissionManager: permissionManager,
1632+
removePermission: { [weak tabViewModel] permissionType in
1633+
tabViewModel?.tab.permissions.remove(permissionType)
1634+
},
1635+
dismissPopover: { [weak self] in
1636+
self?.permissionCenterPopover?.close()
1637+
self?.permissionCenterPopover = nil
1638+
}
1639+
)
1640+
1641+
let popover = PermissionCenterPopover(viewModel: viewModel)
1642+
permissionCenterPopover = popover
1643+
1644+
popover.show(relativeTo: permissionCenterButton.bounds, of: permissionCenterButton, preferredEdge: .maxY)
16141645
}
16151646

16161647
@IBAction func cameraButtonAction(_ sender: NSButton) {
@@ -2296,9 +2327,7 @@ extension TabViewModel {
22962327
let hasRequestedPermission = usedPermissions.values.contains(where: { $0.isRequested
22972328
})
22982329
let shouldShowWhileFocused = (tab.content == .newtab) && hasRequestedPermission
2299-
let isAnyPermissionPresent = usedPermissions.values.contains(where: {
2300-
!$0.isReloading
2301-
})
2330+
let isAnyPermissionPresent = !usedPermissions.values.isEmpty
23022331

23032332
return (shouldShowWhileFocused || (!isTextFieldEditorFirstResponder && isAnyPermissionPresent))
23042333
&& !isAnyTrackerAnimationPlaying

0 commit comments

Comments
 (0)