Skip to content

Commit 848f6d0

Browse files
authored
Use builder to create SheetBrowsingMenu (#2698)
<!-- 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/task/1212219459510183?focus=true Tech Design URL: CC: ### Description This PR adds a foundation for being able to produce multiple variants of Sheet browsing menu using existing entries. In order to allow this the following changes has been made: * Prepare a new protocol to use in sheet variants of the new menu. * Use `BrowsingMenuContext` in MainViewController to encapsulate MVC logic. * Create "A" variant of the menu which is a verbatim version of production. * Add an Appearance Setting and local storage for selecting variant option of the sheet menu (internal only). * Update the model used in `BrowsingMenuSheetView` and improve UI. ### Testing Steps 1. Turn off internal user flag, there should be no additional options in Appearance Settings 2. Do a smoke check around BrowsingMenu - how it looks in different "contexts": NTP, Duck.ai, browsing. 3. Do a general smoke check of Browsing menu. <!-- ### Testability Challenges If you encountered issues writing tests due to any class in the codebase, please report it in the [Testability Challenges Document](https://app.asana.com/1/137249556945/project/1204186595873227/task/1211703869786699) 1. **Check the Document:** First, check the **Testability Challenges Table** to see if the class you encountered is listed. 2. **If the Class Exists:** - Update the **Encounter Count** by increasing it by 1. 4. **If the Class Does Not Exist:** - Add a new entry --> ### Impact and Risks Medium: Could disrupt specific features or user flows #### What could go wrong? 1. Browsing menu does not look properly in different contexts (NTP, browsing, etc.) - this is not likely because the logic was encapsulated and used 1:1 comparing to previous state. 2. Additional options in Appearance Settings are visible for non-internal users - was checked in previous PR and this builds in top of existing state. ### Quality Considerations <!-- Focus on what matters for your changes: - What edge cases exist? - How does this affect performance? - What monitoring have you added? - What documentation needs updating? - How does this impact privacy/security? --> ### Notes to Reviewer There will be a follow-up PR containing additional menu variants: #2691 --- ###### Internal references: [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f) | [Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) | [Tech Design Template](https://app.asana.com/0/59792373528535/184709971311943) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a builder-driven sheet Browsing Menu (variants A–D) with internal-only toggle and variant picker, refactors menu construction and integrates sheet presentation across contexts. > > - **Browsing Menu Sheet Capability & Variants** > - Add `BrowsingMenuSheetCapability` (internal-only, iOS ≥17) with enable/disable and persisted `BrowsingMenuClusteringVariant` (A–D). > - Define `BrowsingMenuVariantBuilder` protocol and implementations: `BrowsingMenuVariantABuilder/B/C/DBuilder`. > - Introduce `BrowsingMenuContext` and `BrowsingMenuEntryBuilding` to decouple menu entry construction. > - **SwiftUI Sheet Menu UI** > - Create `BrowsingMenuModel` and refactor `BrowsingMenuSheetView` to use header/sections/footer, optional row highlight, and floating toolbar; add `List.compactSectionSpacingIfAvailable`. > - **Integration** > - Main flow: determine context (NTP/AI tab/website) and launch either classic or new sheet menu; add favorite-row highlighting via tag. > - Pass capability into `SettingsViewModel`; instantiate capability in `MainViewController`. > - `TabViewController`: expose entry-making methods and `buildSheetBrowsingMenu` using selected variant; factor helpers (share, reload, report, etc.). > - **Settings** > - Add Appearance section controls (internal users): toggle "Sheet menu presentation" and pick "Menu variant"; wire bindings (`showMenuInSheet`, `sheetMenuVariant`). > - Extend `SettingsState` with `showMenuInSheet` and `sheetMenuVariant`; update `SettingsViewModel` init/bindings. > - **Misc** > - Minor utility: `BrowsingMenuEntry.isSeparator` extension; project adds new Swift files and build phases. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e3ddb2f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 28f46c6 commit 848f6d0

16 files changed

+1406
-157
lines changed

iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,12 +450,18 @@
450450
6F40D15E2C34436500BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */; };
451451
6F4BFD3E2DE06F1F00E754EB /* StyledTopBottomBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4BFD3D2DE06F1F00E754EB /* StyledTopBottomBorderView.swift */; };
452452
6F5041C92CC11A5100989E48 /* NewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* NewTabPageView.swift */; };
453+
6F55AE5A2ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE592ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift */; };
454+
6F55AE612ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */; };
455+
6F55AE622ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */; };
456+
6F55AE642ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */; };
453457
6F55BCF92ECE29B3009E50C1 /* BrowsingMenuSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */; };
454458
6F5938982DB1028200C8C068 /* BrowserChromeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5938972DB1028200C8C068 /* BrowserChromeButton.swift */; };
455459
6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */; };
460+
6F5B2FD92EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */; };
456461
6F5DCFCE2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5DCFCD2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift */; };
457462
6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; };
458463
6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; };
464+
6F6DACE32ED70B5E00DB841C /* BrowsingMenuSheetCapability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6DACE22ED70B5200DB841C /* BrowsingMenuSheetCapability.swift */; };
459465
6F72353D2D3EBCB800710C07 /* WebViewStateRestorationDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F72353C2D3EBCB000710C07 /* WebViewStateRestorationDebugView.swift */; };
460466
6F76B5D62EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F76B5D52EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift */; };
461467
6F7BACD42CEE084B00F561D8 /* OmniBarEqualityCheckTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F7BACD32CEE084100F561D8 /* OmniBarEqualityCheckTests.swift */; };
@@ -477,6 +483,7 @@
477483
6FB16A262E3A2682006E361C /* SwipeContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB16A252E3A267E006E361C /* SwipeContainerViewController.swift */; };
478484
6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */; };
479485
6FB2A6802C2EA950004D20C8 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */; };
486+
6FB864C52ED0CE1500E8333F /* ListExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB864C42ED0CE1100E8333F /* ListExtension.swift */; };
480487
6FBE76F52DCB7BFB00D3FF0A /* NewTabPageShadowScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBE76F42DCB7BFB00D3FF0A /* NewTabPageShadowScrollView.swift */; };
481488
6FBF0F8B2BD7C0A900136CF0 /* AllProtectedCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */; };
482489
6FCF65142DF8481F00C7F35F /* OmniBarEditingStateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCF65132DF8481400C7F35F /* OmniBarEditingStateViewController.swift */; };
@@ -2569,12 +2576,18 @@
25692576
6F40D15C2C34436200BF22F0 /* HomePageDisplayDailyPixelBucketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageDisplayDailyPixelBucketTests.swift; sourceTree = "<group>"; };
25702577
6F4BFD3D2DE06F1F00E754EB /* StyledTopBottomBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyledTopBottomBorderView.swift; sourceTree = "<group>"; };
25712578
6F5041C82CC11A5100989E48 /* NewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageView.swift; sourceTree = "<group>"; };
2579+
6F55AE592ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantBuilder.swift; sourceTree = "<group>"; };
2580+
6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantABuilder.swift; sourceTree = "<group>"; };
2581+
6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantBBuilder.swift; sourceTree = "<group>"; };
2582+
6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantCBuilder.swift; sourceTree = "<group>"; };
25722583
6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuSheetView.swift; sourceTree = "<group>"; };
25732584
6F5938972DB1028200C8C068 /* BrowserChromeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserChromeButton.swift; sourceTree = "<group>"; };
25742585
6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapterTests.swift; sourceTree = "<group>"; };
2586+
6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantDBuilder.swift; sourceTree = "<group>"; };
25752587
6F5DCFCD2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarEditingStateTransition.swift; sourceTree = "<group>"; };
25762588
6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = "<group>"; };
25772589
6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = "<group>"; };
2590+
6F6DACE22ED70B5200DB841C /* BrowsingMenuSheetCapability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuSheetCapability.swift; sourceTree = "<group>"; };
25782591
6F72353C2D3EBCB000710C07 /* WebViewStateRestorationDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewStateRestorationDebugView.swift; sourceTree = "<group>"; };
25792592
6F76B5D52EAA6EC00027C425 /* SettingsAutoClearActionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAutoClearActionDelegate.swift; sourceTree = "<group>"; };
25802593
6F7BACD32CEE084100F561D8 /* OmniBarEqualityCheckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarEqualityCheckTests.swift; sourceTree = "<group>"; };
@@ -2597,6 +2610,7 @@
25972610
6FB16A252E3A267E006E361C /* SwipeContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeContainerViewController.swift; sourceTree = "<group>"; };
25982611
6FB2A67D2C2DAFB4004D20C8 /* NewTabPageGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageGridView.swift; sourceTree = "<group>"; };
25992612
6FB2A67F2C2EA950004D20C8 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = "<group>"; };
2613+
6FB864C42ED0CE1100E8333F /* ListExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListExtension.swift; sourceTree = "<group>"; };
26002614
6FBE76F42DCB7BFB00D3FF0A /* NewTabPageShadowScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageShadowScrollView.swift; sourceTree = "<group>"; };
26012615
6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = "<group>"; };
26022616
6FCF65132DF8481400C7F35F /* OmniBarEditingStateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmniBarEditingStateViewController.swift; sourceTree = "<group>"; };
@@ -4741,6 +4755,7 @@
47414755
1E162603296840790004127F /* SwiftUI */ = {
47424756
isa = PBXGroup;
47434757
children = (
4758+
6FB864C42ED0CE1100E8333F /* ListExtension.swift */,
47444759
1E24295D293F57FA00584836 /* LottieView.swift */,
47454760
1E162604296840D80004127F /* Triangle.swift */,
47464761
1E1626062968413B0004127F /* ViewExtension.swift */,
@@ -5858,7 +5873,13 @@
58585873
6F55BCF72ECE299A009E50C1 /* SheetPresentationMenu */ = {
58595874
isa = PBXGroup;
58605875
children = (
5876+
6F55AE592ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift */,
58615877
6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */,
5878+
6F6DACE22ED70B5200DB841C /* BrowsingMenuSheetCapability.swift */,
5879+
6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */,
5880+
6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */,
5881+
6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */,
5882+
6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */,
58625883
);
58635884
path = SheetPresentationMenu;
58645885
sourceTree = "<group>";
@@ -10882,6 +10903,7 @@
1088210903
3157B43527F497F50042D3D7 /* SaveLoginViewController.swift in Sources */,
1088310904
317A247F2D8336650033A0D6 /* DownloadsDirectoryHandling.swift in Sources */,
1088410905
853807922D6E638000CE1455 /* AlertPlaygroundView.swift in Sources */,
10906+
6F55AE642ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift in Sources */,
1088510907
C14D43012B45D6CD00ACA4DC /* AutofillDebugViewController.swift in Sources */,
1088610908
856F28D72D3EC5FA00A88211 /* TabSwitcherViewController+MultiSelect.swift in Sources */,
1088710909
853C5F6121C277C7001F7A05 /* global.swift in Sources */,
@@ -11074,6 +11096,8 @@
1107411096
9F23B8032C2BCD0000950875 /* DaxDialogStyles.swift in Sources */,
1107511097
C128BA252DF0847900ADDC5F /* SettingsCompleteSetupView.swift in Sources */,
1107611098
C1C18BF62D956FAD00FFBA2E /* AutofillEditableCell.swift in Sources */,
11099+
6F55AE612ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift in Sources */,
11100+
6F55AE622ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift in Sources */,
1107711101
65BD16352D8C206F000795DE /* DuckPlayerPrimingModalView.swift in Sources */,
1107811102
65BD16362D8C206F000795DE /* DuckPlayerPrimingModalViewModel.swift in Sources */,
1107911103
C1641EAF2BC2F5140012607A /* ImportPasswordsViaSyncViewController.swift in Sources */,
@@ -11308,6 +11332,7 @@
1130811332
6566A5D02D775D560033283C /* DuckPlayerMiniPillViewModel.swift in Sources */,
1130911333
F4B0B78C252CAFF700830156 /* OnboardingWidgetsViewController.swift in Sources */,
1131011334
7BFD5FD52C9DA310000FF959 /* VPNAddWidgetTip.swift in Sources */,
11335+
6F55AE5A2ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift in Sources */,
1131111336
31BB23A62D3A96F000809ABF /* DeepLinks.swift in Sources */,
1131211337
C17B595A2A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift in Sources */,
1131311338
8531A08E1F9950E6000484F0 /* UnprotectedSitesViewController.swift in Sources */,
@@ -11379,6 +11404,7 @@
1137911404
6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */,
1138011405
9FBE93BC2EB040F5003E74FB /* ShareAction+UIActivity.swift in Sources */,
1138111406
C1FA13652ECFA067001392FA /* AutofillExtensionEnableCoordinator.swift in Sources */,
11407+
6FB864C52ED0CE1500E8333F /* ListExtension.swift in Sources */,
1138211408
7B4F87EA2D0738F90010B18F /* SiriEducationView.swift in Sources */,
1138311409
AA4D6A6A23DB87B1007E8790 /* AppIconManager.swift in Sources */,
1138411410
8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */,
@@ -11446,6 +11472,7 @@
1144611472
0283A2012C6E46E300508FBD /* BrokenSitePromptLimiterStore.swift in Sources */,
1144711473
85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */,
1144811474
851672D32BED23FE00592F24 /* AutocompleteViewModel.swift in Sources */,
11475+
6F6DACE32ED70B5E00DB841C /* BrowsingMenuSheetCapability.swift in Sources */,
1144911476
CBF259922D47BFDD00AC63E4 /* OverlayWindowManager.swift in Sources */,
1145011477
CC6A609A2EA63731003A0398 /* VPNSubscriptionPromotionHelper.swift in Sources */,
1145111478
9F012C632DF7BD9D00AE0F43 /* AudioSession.swift in Sources */,
@@ -11462,6 +11489,7 @@
1146211489
CB7E0A232D5E1E55002A7C0C /* LaunchActionHandler.swift in Sources */,
1146311490
97770B702EC1ED2600CACA68 /* OnboardingSearchExperiencePicker.swift in Sources */,
1146411491
97770B712EC1ED2600CACA68 /* OnboardingSearchExperiencePickerViewModel.swift in Sources */,
11492+
6F5B2FD92EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift in Sources */,
1146511493
97770B722EC1ED2600CACA68 /* OnboardingSearchExperienceProvider.swift in Sources */,
1146611494
97770B732EC1ED2600CACA68 /* OnboardingSearchExperienceSelectionHandler.swift in Sources */,
1146711495
C189966E2E4F45A200246F22 /* Logger+DaxEasterEgg.swift in Sources */,

iOS/DuckDuckGo/BrowsingMenu/BrowsingMenuViewController.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,12 @@ extension BrowsingMenuViewController {
362362
}
363363
}
364364
}
365+
366+
extension BrowsingMenuEntry {
367+
var isSeparator: Bool {
368+
switch self {
369+
case .separator: return true
370+
default: return false
371+
}
372+
}
373+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// BrowsingMenuSheetCapability.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+
import BrowserServicesKit
21+
import Persistence
22+
import Foundation
23+
import Core
24+
25+
protocol BrowsingMenuSheetCapable {
26+
var isAvailable: Bool { get }
27+
var isEnabled: Bool { get }
28+
var variant: BrowsingMenuClusteringVariant { get set }
29+
30+
@discardableResult
31+
func setEnabled(_ enabled: Bool) -> Bool
32+
}
33+
34+
enum BrowsingMenuClusteringVariant: String, CaseIterable, CustomStringConvertible {
35+
var description: String {
36+
switch self {
37+
case .a:
38+
"Production"
39+
case .b:
40+
"Easy Shortcuts"
41+
case .c:
42+
"Easy Privacy Tools"
43+
case .d:
44+
"Easy Privacy - No floating button"
45+
}
46+
}
47+
48+
case a
49+
case b
50+
case c
51+
case d
52+
}
53+
54+
enum BrowsingMenuSheetCapability {
55+
static func create(using featureFlagger: FeatureFlagger, keyValueStore: ThrowingKeyValueStoring) -> BrowsingMenuSheetCapable {
56+
if #available(iOS 17, *) {
57+
return BrowsingMenuSheetDefaultCapability(featureFlagger: featureFlagger, keyValueStore: keyValueStore)
58+
} else {
59+
return BrowsingMenuSheetUnavailableCapability()
60+
}
61+
}
62+
}
63+
64+
struct BrowsingMenuSheetUnavailableCapability: BrowsingMenuSheetCapable {
65+
let isAvailable: Bool = false
66+
let isEnabled: Bool = false
67+
var variant: BrowsingMenuClusteringVariant = .a
68+
69+
func setEnabled(_ enabled: Bool) -> Bool {
70+
false
71+
}
72+
}
73+
74+
@available(iOS 17.0, *)
75+
struct BrowsingMenuSheetDefaultCapability: BrowsingMenuSheetCapable {
76+
let featureFlagger: FeatureFlagger
77+
private let keyValueStore: ThrowingKeyValueStoring
78+
79+
init(featureFlagger: FeatureFlagger, keyValueStore: ThrowingKeyValueStoring) {
80+
self.featureFlagger = featureFlagger
81+
self.keyValueStore = keyValueStore
82+
}
83+
84+
var isAvailable: Bool {
85+
return featureFlagger.internalUserDecider.isInternalUser
86+
}
87+
88+
var isEnabled: Bool {
89+
guard isAvailable else { return false }
90+
91+
return featureFlagger.isFeatureOn(.browsingMenuSheetPresentation)
92+
}
93+
94+
var variant: BrowsingMenuClusteringVariant {
95+
get {
96+
if let variant = try? keyValueStore.object(forKey: StorageKey.menuVariant) as? String {
97+
return BrowsingMenuClusteringVariant(rawValue: variant) ?? .a
98+
} else {
99+
return .a
100+
}
101+
}
102+
set {
103+
try? keyValueStore.set(newValue.rawValue, forKey: StorageKey.menuVariant)
104+
}
105+
}
106+
107+
func setEnabled(_ enabled: Bool) -> Bool {
108+
109+
guard isAvailable else { return false }
110+
111+
let flag = FeatureFlag.browsingMenuSheetPresentation
112+
if let overrides = self.featureFlagger.localOverrides,
113+
overrides.override(for: flag) != enabled {
114+
115+
overrides.toggleOverride(for: flag)
116+
}
117+
118+
return isEnabled
119+
}
120+
121+
private struct StorageKey {
122+
static let menuVariant = "browsingMenuVariantKey"
123+
}
124+
}

0 commit comments

Comments
 (0)