Skip to content

Commit 5b28e89

Browse files
authored
HACK Week: New screen for notification settings (#15373)
2 parents e514d68 + e7dbb9a commit 5b28e89

File tree

8 files changed

+217
-5
lines changed

8 files changed

+217
-5
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
9595
return buildConfig == .localDeveloper || buildConfig == .alpha
9696
case .backgroundProductImageUpload:
9797
return buildConfig == .localDeveloper || buildConfig == .alpha
98+
case .notificationSettings:
99+
return buildConfig == .localDeveloper || buildConfig == .alpha
98100
default:
99101
return true
100102
}

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,8 @@ public enum FeatureFlag: Int {
204204
/// Supports uploading product images in background
205205
///
206206
case backgroundProductImageUpload
207+
208+
/// Supports managing notification settings from the app settings
209+
///
210+
case notificationSettings
207211
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import SwiftUI
2+
3+
final class NotificationSettingsHostingController: UIHostingController<NotificationSettingsView> {
4+
init() {
5+
super.init(rootView: NotificationSettingsView())
6+
}
7+
8+
required dynamic init?(coder aDecoder: NSCoder) {
9+
fatalError("init(coder:) has not been implemented")
10+
}
11+
12+
override func viewDidLoad() {
13+
super.viewDidLoad()
14+
title = NotificationSettingsView.Localization.title
15+
}
16+
}
17+
18+
struct NotificationSettingsView: View {
19+
@State private var notificationsEnabled = false
20+
@State private var orderNotificationsEnabled = false
21+
@State private var productReviewNotificationsEnabled = false
22+
23+
var body: some View {
24+
Form {
25+
Section {
26+
Toggle(isOn: $notificationsEnabled) {
27+
Text(Localization.allNotifications)
28+
}
29+
} footer: {
30+
Text(Localization.allNotificationsFooter)
31+
}
32+
33+
Section {
34+
Toggle(isOn: $orderNotificationsEnabled) {
35+
Text(Localization.newOrders)
36+
}
37+
.disabled(!notificationsEnabled)
38+
39+
Toggle(isOn: $productReviewNotificationsEnabled) {
40+
Text(Localization.productReviews)
41+
}
42+
.disabled(!notificationsEnabled)
43+
} header: {
44+
Text(Localization.notificationTypesHeader)
45+
} footer: {
46+
Text(Localization.notificationTypesFooter)
47+
}
48+
}
49+
.navigationTitle(Localization.title)
50+
}
51+
}
52+
53+
extension NotificationSettingsView {
54+
enum Localization {
55+
static let title = NSLocalizedString(
56+
"notificationSettingsView.title",
57+
value: "Notification Settings",
58+
comment: "Title of the notification settings view"
59+
)
60+
static let allNotifications = NSLocalizedString(
61+
"notificationSettingsView.allNotifications",
62+
value: "All notifications",
63+
comment: "Label of the toggle to enable/disable all notifications on the notification settings view"
64+
)
65+
static let allNotificationsFooter = NSLocalizedString(
66+
"notificationSettingsView.allNotificationsFooter",
67+
value: "Including in-app reminders and remote push notifications.",
68+
comment: "Footer of the toggle to enable/disable all notifications on the notification settings view"
69+
)
70+
static let newOrders = NSLocalizedString(
71+
"notificationSettingsView.newOrders",
72+
value: "New orders",
73+
comment: "Label of the toggle to enable/disable new order notifications on the notification settings view"
74+
)
75+
static let productReviews = NSLocalizedString(
76+
"notificationSettingsView.productReviews",
77+
value: "Product reviews",
78+
comment: "Label of the toggle to enable/disable product reviews notifications on the notification settings view"
79+
)
80+
static let notificationTypesHeader = NSLocalizedString(
81+
"notificationSettingsView.notificationTypesHeader",
82+
value: "Notification types",
83+
comment: "Header of the notification types section on the notification settings view"
84+
)
85+
static let notificationTypesFooter = NSLocalizedString(
86+
"notificationSettingsView.notificationTypesFooter",
87+
value: "Settings applied to all selected sites.",
88+
comment: "Footer of the notification types section on the notification settings view"
89+
)
90+
}
91+
}
92+
93+
#Preview {
94+
NotificationSettingsView()
95+
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewController.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ private extension SettingsViewController {
156156
configureBetaFeatures(cell: cell)
157157
case let cell as BasicTableViewCell where row == .sendFeedback:
158158
configureSendFeedback(cell: cell)
159+
case let cell as BasicTableViewCell where row == .notifications:
160+
configureNotificationSettings(cell: cell)
159161
case let cell as BasicTableViewCell where row == .privacy:
160162
configurePrivacy(cell: cell)
161163
case let cell as BasicTableViewCell where row == .about:
@@ -229,6 +231,12 @@ private extension SettingsViewController {
229231
cell.textLabel?.text = Localization.storeName
230232
}
231233

234+
func configureNotificationSettings(cell: BasicTableViewCell) {
235+
cell.accessoryType = .disclosureIndicator
236+
cell.selectionStyle = .default
237+
cell.textLabel?.text = Localization.notificationSettings
238+
}
239+
232240
func configurePrivacy(cell: BasicTableViewCell) {
233241
cell.accessoryType = .disclosureIndicator
234242
cell.selectionStyle = .default
@@ -476,6 +484,11 @@ private extension SettingsViewController {
476484
present(surveyNavigation, animated: true, completion: nil)
477485
}
478486

487+
func showNotificationSettings() {
488+
let controller = NotificationSettingsHostingController()
489+
show(controller, sender: self)
490+
}
491+
479492
func deviceSettingsWasPressed() {
480493
guard let targetURL = URL(string: UIApplication.openSettingsURLString) else {
481494
return
@@ -653,6 +666,8 @@ extension SettingsViewController: UITableViewDelegate {
653666
logoutWasPressed()
654667
case .themes:
655668
showThemeSettings()
669+
case .notifications:
670+
showNotificationSettings()
656671
default:
657672
break
658673
}
@@ -719,6 +734,7 @@ extension SettingsViewController {
719734
case sendFeedback
720735

721736
// App Settings
737+
case notifications
722738
case privacy
723739

724740
// About the App
@@ -762,7 +778,7 @@ extension SettingsViewController {
762778
return BasicTableViewCell.self
763779
case .logout, .accountSettings:
764780
return BasicTableViewCell.self
765-
case .privacy:
781+
case .privacy, .notifications:
766782
return BasicTableViewCell.self
767783
case .betaFeatures:
768784
return BasicTableViewCell.self
@@ -853,6 +869,12 @@ private extension SettingsViewController {
853869
comment: "Navigates to Privacy Settings screen"
854870
)
855871

872+
static let notificationSettings = NSLocalizedString(
873+
"settingsViewController.notificationSettings",
874+
value: "Notification Settings",
875+
comment: "Navigates to the Notification Settings screen"
876+
)
877+
856878
static let experimentalFeatures = NSLocalizedString(
857879
"Experimental Features",
858880
comment: "Navigates to experimental features screen"

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Settings/SettingsViewModel.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,26 @@ private extension SettingsViewModel {
277277
}()
278278

279279
// App Settings
280-
let appSettingsSection = Section(title: Localization.appSettingsTitle,
281-
rows: [.privacy],
282-
footerHeight: UITableView.automaticDimension)
280+
let appSettingsSection: Section = {
281+
let rows: [Row]
282+
let notificationAvailable: Bool = {
283+
guard stores.isAuthenticated && stores.isAuthenticatedWithoutWPCom == false else {
284+
return false
285+
}
286+
guard let site = stores.sessionManager.defaultSite else {
287+
return false
288+
}
289+
return site.isJetpackCPConnected == false
290+
}()
291+
if notificationAvailable, featureFlagService.isFeatureFlagEnabled(.notificationSettings) {
292+
rows = [.notifications, .privacy]
293+
} else {
294+
rows = [.privacy]
295+
}
296+
return Section(title: Localization.appSettingsTitle,
297+
rows: rows,
298+
footerHeight: UITableView.automaticDimension)
299+
}()
283300

284301
// About the App
285302
let aboutTheAppSection: Section = {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,6 +2680,7 @@
26802680
DE5746342B4512900034B10D /* BlazeBudgetSettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5746332B4512900034B10D /* BlazeBudgetSettingView.swift */; };
26812681
DE5746362B4522ED0034B10D /* BlazeBudgetSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5746352B4522ED0034B10D /* BlazeBudgetSettingViewModel.swift */; };
26822682
DE5746382B479CB80034B10D /* BlazeBudgetSettingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5746372B479CB80034B10D /* BlazeBudgetSettingViewModelTests.swift */; };
2683+
DE58C8A42D88226A005914DF /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE58C8A32D88226A005914DF /* NotificationSettingsView.swift */; };
26832684
DE5C19C92AC42E7E0064600A /* WooAnalyticsEvent+ProductNameAI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5C19C82AC42E7E0064600A /* WooAnalyticsEvent+ProductNameAI.swift */; };
26842685
DE5FBB912A9EF25A0072FB35 /* WooPaymentSetupWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5FBB902A9EF25A0072FB35 /* WooPaymentSetupWebViewModel.swift */; };
26852686
DE5FBB932A9EFBCC0072FB35 /* WooPaymentSetupWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE5FBB922A9EFBCC0072FB35 /* WooPaymentSetupWebViewModelTests.swift */; };
@@ -5888,6 +5889,7 @@
58885889
DE5746332B4512900034B10D /* BlazeBudgetSettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeBudgetSettingView.swift; sourceTree = "<group>"; };
58895890
DE5746352B4522ED0034B10D /* BlazeBudgetSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeBudgetSettingViewModel.swift; sourceTree = "<group>"; };
58905891
DE5746372B479CB80034B10D /* BlazeBudgetSettingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeBudgetSettingViewModelTests.swift; sourceTree = "<group>"; };
5892+
DE58C8A32D88226A005914DF /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
58915893
DE5C19C82AC42E7E0064600A /* WooAnalyticsEvent+ProductNameAI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+ProductNameAI.swift"; sourceTree = "<group>"; };
58925894
DE5FBB902A9EF25A0072FB35 /* WooPaymentSetupWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentSetupWebViewModel.swift; sourceTree = "<group>"; };
58935895
DE5FBB922A9EFBCC0072FB35 /* WooPaymentSetupWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentSetupWebViewModelTests.swift; sourceTree = "<group>"; };
@@ -12423,6 +12425,7 @@
1242312425
CE85FD5820F7A59E0080B73E /* Settings */ = {
1242412426
isa = PBXGroup;
1242512427
children = (
12428+
DE58C8A22D882250005914DF /* NotificationSettings */,
1242612429
DE6F997A2BE9E5CA0007B2DD /* Settings.storyboard */,
1242712430
DEDA8DBA2B19833E0076BF0F /* Themes */,
1242812431
DE68979D2A8F7C8C00154588 /* AccountSettings */,
@@ -13275,6 +13278,14 @@
1327513278
path = BudgetSetting;
1327613279
sourceTree = "<group>";
1327713280
};
13281+
DE58C8A22D882250005914DF /* NotificationSettings */ = {
13282+
isa = PBXGroup;
13283+
children = (
13284+
DE58C8A32D88226A005914DF /* NotificationSettingsView.swift */,
13285+
);
13286+
path = NotificationSettings;
13287+
sourceTree = "<group>";
13288+
};
1327813289
DE63115D2AF1E16500587641 /* WPComLogin */ = {
1327913290
isa = PBXGroup;
1328013291
children = (
@@ -16931,6 +16942,7 @@
1693116942
26BCA0402C35E9A9000BE96C /* BackgroundTaskRefreshDispatcher.swift in Sources */,
1693216943
DECEA4472C81778300C28C10 /* ProductImagePickerView.swift in Sources */,
1693316944
26E0AE1926335AA900A5EB3B /* Survey.swift in Sources */,
16945+
DE58C8A42D88226A005914DF /* NotificationSettingsView.swift in Sources */,
1693416946
0371C3682875E47B00277E2C /* FeatureAnnouncementCardViewModel.swift in Sources */,
1693516947
CE755F732D4A5F9D002539F6 /* WooShippingNormalizeAddressViewModel.swift in Sources */,
1693616948
B90D21782D15B72900ED60ED /* WooShippingCustomsFormViewModel.swift in Sources */,

WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class MockFeatureFlagService: FeatureFlagService {
2323
var favoriteProducts: Bool
2424
var isProductGlobalUniqueIdentifierSupported: Bool
2525
var hideSitesInStorePicker: Bool
26+
var notificationSettings: Bool
2627

2728
init(isInboxOn: Bool = false,
2829
isShowInboxCTAEnabled: Bool = false,
@@ -44,7 +45,8 @@ final class MockFeatureFlagService: FeatureFlagService {
4445
viewEditCustomFieldsInProductsAndOrders: Bool = false,
4546
favoriteProducts: Bool = false,
4647
isProductGlobalUniqueIdentifierSupported: Bool = false,
47-
hideSitesInStorePicker: Bool = false) {
48+
hideSitesInStorePicker: Bool = false,
49+
notificationSettings: Bool = false) {
4850
self.isInboxOn = isInboxOn
4951
self.isShowInboxCTAEnabled = isShowInboxCTAEnabled
5052
self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn
@@ -66,6 +68,7 @@ final class MockFeatureFlagService: FeatureFlagService {
6668
self.favoriteProducts = favoriteProducts
6769
self.isProductGlobalUniqueIdentifierSupported = isProductGlobalUniqueIdentifierSupported
6870
self.hideSitesInStorePicker = hideSitesInStorePicker
71+
self.notificationSettings = notificationSettings
6972
}
7073

7174
func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool {
@@ -112,6 +115,8 @@ final class MockFeatureFlagService: FeatureFlagService {
112115
return isProductGlobalUniqueIdentifierSupported
113116
case .hideSitesInStorePicker:
114117
return hideSitesInStorePicker
118+
case .notificationSettings:
119+
return notificationSettings
115120
default:
116121
return false
117122
}

WooCommerce/WooCommerceTests/ViewRelated/Settings/SettingsViewModelTests.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,61 @@ final class SettingsViewModelTests: XCTestCase {
313313
XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.whatsNew) })
314314
}
315315

316+
func test_sections_does_not_contain_notifications_row_when_feature_flag_is_disabled() {
317+
// Given
318+
let featureFlagService = MockFeatureFlagService(notificationSettings: false)
319+
let testSite = Site.fake().copy(siteID: 123, isJetpackThePluginInstalled: true, isJetpackConnected: true)
320+
let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true, defaultSite: testSite))
321+
let viewModel = SettingsViewModel(stores: stores, featureFlagService: featureFlagService)
322+
323+
// When
324+
viewModel.onViewDidLoad()
325+
326+
// Then
327+
XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.notifications) })
328+
}
329+
330+
func test_sections_does_not_contain_notifications_row_when_user_is_authenticated_without_WPCom() {
331+
// Given
332+
let featureFlagService = MockFeatureFlagService(notificationSettings: true)
333+
let testSite = Site.fake().copy(siteID: 123, isJetpackThePluginInstalled: true, isJetpackConnected: true)
334+
let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: false, defaultSite: testSite))
335+
let viewModel = SettingsViewModel(stores: stores, featureFlagService: featureFlagService)
336+
337+
// When
338+
viewModel.onViewDidLoad()
339+
340+
// Then
341+
XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.notifications) })
342+
}
343+
344+
func test_sections_does_not_contain_notifications_row_when_site_is_JCP() {
345+
// Given
346+
let featureFlagService = MockFeatureFlagService(notificationSettings: true)
347+
let testSite = Site.fake().copy(siteID: 123, isJetpackThePluginInstalled: false, isJetpackConnected: true)
348+
let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true, defaultSite: testSite))
349+
let viewModel = SettingsViewModel(stores: stores, featureFlagService: featureFlagService)
350+
351+
// When
352+
viewModel.onViewDidLoad()
353+
354+
// Then
355+
XCTAssertFalse(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.notifications) })
356+
}
357+
358+
func test_sections_does_not_contain_notifications_row_for_Jetpack_site_and_user_is_authenticated_with_WPCom() {
359+
// Given
360+
let featureFlagService = MockFeatureFlagService(notificationSettings: true)
361+
let testSite = Site.fake().copy(siteID: 123, isJetpackThePluginInstalled: true, isJetpackConnected: true)
362+
let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: true, isWPCom: true, defaultSite: testSite))
363+
let viewModel = SettingsViewModel(stores: stores, featureFlagService: featureFlagService)
364+
365+
// When
366+
viewModel.onViewDidLoad()
367+
368+
// Then
369+
XCTAssertTrue(viewModel.sections.contains { $0.rows.contains(SettingsViewController.Row.notifications) })
370+
}
316371
}
317372

318373
private final class MockSettingsPresenter: SettingsViewPresenter {

0 commit comments

Comments
 (0)