From 5ae227179c9b573a8f6c82e6b52547710f9f6943 Mon Sep 17 00:00:00 2001 From: Nicolas Mauri Date: Mon, 10 Jul 2023 19:26:49 +0200 Subject: [PATCH] New notifications section in settings (#1273) --- ElementX.xcodeproj/project.pbxproj | 56 ++++++++++++ .../Sources/Application/AppCoordinator.swift | 10 ++- .../Sources/Application/AppSettings.swift | 8 ++ .../Mocks/Generated/GeneratedMocks.swift | 82 +++++++++++++++++ .../DeveloperOptionsScreenModels.swift | 2 + .../DeveloperOptionsScreenViewModel.swift | 5 +- .../View/DeveloperOptionsScreen.swift | 7 ++ ...otificationSettingsScreenCoordinator.swift | 48 ++++++++++ .../NotificationSettingsScreenModels.swift | 53 +++++++++++ .../NotificationSettingsScreenViewModel.swift | 76 ++++++++++++++++ ...ationSettingsScreenViewModelProtocol.swift | 23 +++++ .../View/NotificationSettingsScreen.swift | 88 +++++++++++++++++++ .../SettingsScreenCoordinator.swift | 10 +++ .../SettingsScreen/SettingsScreenModels.swift | 3 + .../SettingsScreenViewModel.swift | 7 ++ .../SettingsScreen/View/SettingsScreen.swift | 10 +++ .../UNUserNotificationCenter+Settings.swift | 24 +++++ .../Manager/NotificationManager.swift | 43 +++++++-- .../Manager/NotificationManagerProtocol.swift | 4 +- .../UserNotificationCenterProtocol.swift | 4 + .../UITests/UITestsAppCoordinator.swift | 5 ++ .../UITests/UITestsScreenIdentifier.swift | 1 + .../NotificationSettingsScreenUITests.swift | 21 +++++ .../NotificationManagerTests.swift | 8 +- .../UserNotificationCenterSpy.swift | 5 ++ ...ficationSettingsScreenViewModelTests.swift | 50 +++++++++++ 26 files changed, 639 insertions(+), 14 deletions(-) create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift create mode 100644 ElementX/Sources/Services/Notification/Helper/UNUserNotificationCenter+Settings.swift create mode 100644 UITests/Sources/NotificationSettingsScreenUITests.swift create mode 100644 UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f028c185a..61e2aa0ea 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */; }; 1702981A8085BE4FB0EC001B /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D33116993D54FADC0C721C1F /* Application.swift */; }; 172E6E9A612ADCF10A62CF13 /* BugReportServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */; }; + 17780569FB41E9BAC60D4710 /* UNUserNotificationCenter+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; }; 19FE025AE9BA2959B6589B0D /* RoomMemberDetailsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CC575D1895FA62591451A93 /* RoomMemberDetailsScreen.swift */; }; @@ -269,6 +270,7 @@ 664F77F02A57617A00FB9B24 /* XCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664F77EF2A57617A00FB9B24 /* XCTestCase.swift */; }; 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; + 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */; }; 67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; }; 67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; }; 68184EF36396424FE19A727D /* MediaLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */; }; @@ -337,6 +339,7 @@ 7F08F4BC1312075E2B5EAEFA /* AuthenticationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */; }; 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */; }; 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; + 7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */; }; 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; }; @@ -492,6 +495,7 @@ AF19D65A9C60C6B2646F3210 /* RedactedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6E6BDF9D26DB05C88901416 /* RedactedRoomTimelineItem.swift */; }; AF2ABA2794E376B64104C964 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */; }; AF33B9044498211C3D82F1E1 /* UNTextInputNotificationResponse+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */; }; + AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */; }; AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; B037C365CF8A58A0D149A2DB /* AuthenticationIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97755C01C3971474EFAD5367 /* AuthenticationIconImage.swift */; }; B064D42BA087649ACAE462E8 /* SoftLogoutUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F30E764BED111C81739844 /* SoftLogoutUITests.swift */; }; @@ -584,6 +588,7 @@ CB6BCBF28E4B76EA08C2926D /* StateRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16048D30F0438731C41F775 /* StateRoomTimelineItem.swift */; }; CB99B0FA38A4AC596F38CC13 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; CBA9EDF305036039166E76FF /* StartChatScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */; }; + CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */; }; CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */; }; CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */; }; CCAA0671B46EAFD0BB528E2C /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 8FC26871038FB0E4AAE22605 /* apple_emojis_data.json */; }; @@ -598,6 +603,7 @@ CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; }; CF4044A8EED5C41BC0ED6ABE /* SoftLogoutScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D316BB02636AF2174F2580E6 /* SoftLogoutScreenViewModelProtocol.swift */; }; D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */; }; + D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; }; D181AC8FF236B7F91C0A8C28 /* MapTiler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23AA3F4B285570805CB0CCDD /* MapTiler.swift */; }; D2A15D03F81342A09340BD56 /* AnalyticsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFEEE93B82937B2E86F92EB /* AnalyticsScreen.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; @@ -648,6 +654,7 @@ E2DDA49BD62F03F180A42E30 /* MapLibreStaticMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */; }; E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; }; E3291AD16D7A5CB14781819C /* UserNotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */; }; + E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */; }; E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; E3E1E255DC8CB34BD8573E0D /* UserIndicatorControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */; }; E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */; }; @@ -705,6 +712,7 @@ F656F92A63D3DC1978D79427 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; }; F692D4AF571333C0D785725A /* MigrationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */; }; F697284B9B5F2C00CFEA3B12 /* EmojiDetectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58E93D91DE3288010390DEE /* EmojiDetectionTests.swift */; }; + F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; }; @@ -822,6 +830,7 @@ 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; 0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenUITests.swift; sourceTree = ""; }; 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = ""; }; + 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenModels.swift; sourceTree = ""; }; 0FA60F848D1C14F873F9621A /* RoomMemberDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsScreenCoordinator.swift; sourceTree = ""; }; 0FB782CE6176A5D2C082EC5D /* WelcomeScreenScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenScreenModels.swift; sourceTree = ""; }; 1059E2AE7878CF7820592637 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; @@ -885,6 +894,7 @@ 225EFCA26877E75CDFE7F48D /* MapTilerStyleBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyleBuilderProtocol.swift; sourceTree = ""; }; 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = ""; }; 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; + 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; 23AA3F4B285570805CB0CCDD /* MapTiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTiler.swift; sourceTree = ""; }; 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInvitesButton.swift; sourceTree = ""; }; 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerTests.swift; sourceTree = ""; }; @@ -956,6 +966,7 @@ 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenUITests.swift; sourceTree = ""; }; 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreen.swift; sourceTree = ""; }; 421E716C521F96D24ECE69B3 /* NoticeRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItem.swift; sourceTree = ""; }; + 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 422724361B6555364C43281E /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; 42ADEA322D2089391E049535 /* InvitesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreen.swift; sourceTree = ""; }; 42C64A14EE89928207E3B42B /* AnalyticsSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenModels.swift; sourceTree = ""; }; @@ -970,6 +981,7 @@ 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = ""; }; 46B59EC4B0C93254089EAACB /* MigrationScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelTests.swift; sourceTree = ""; }; 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; + 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModel.swift; sourceTree = ""; }; 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; @@ -998,6 +1010,7 @@ 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenViewModelTests.swift; sourceTree = ""; }; 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = ""; }; + 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenViewModelTests.swift; sourceTree = ""; }; 51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenModels.swift; sourceTree = ""; }; 51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileProxy.swift; sourceTree = ""; }; 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenModels.swift; sourceTree = ""; }; @@ -1189,6 +1202,7 @@ 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationStackCoordinatorTests.swift; sourceTree = ""; }; 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9CE3C90E487B255B735D73C8 /* RoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModel.swift; sourceTree = ""; }; + 9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNUserNotificationCenter+Settings.swift"; sourceTree = ""; }; 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1262,6 +1276,7 @@ B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenCoordinator.swift; sourceTree = ""; }; B81B6170DB690013CEB646F4 /* MapLibreModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreModels.swift; sourceTree = ""; }; + B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenUITests.swift; sourceTree = ""; }; B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; @@ -1301,6 +1316,7 @@ C54464351F170D570110AFCA /* WelcomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenUITests.swift; sourceTree = ""; }; + C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = ""; }; C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = ""; }; C618CA2B6C8758B06C88013C /* CreateRoomCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomCoordinator.swift; sourceTree = ""; }; C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; @@ -2326,6 +2342,7 @@ isa = PBXGroup; children = ( C830A64609CBD152F06E0457 /* NotificationConstants.swift */, + A9E072BD31B744FC34F4D368 /* Helper */, 6EE5E2BBFBC7947CFE789B4D /* Manager */, 832FC81F760220239E285294 /* Proxy */, ); @@ -2349,6 +2366,7 @@ 09C599CB430ABF160C1EE55C /* AnalyticsSettingsScreen */, 1CA6CD0DE6F0445156361B6D /* DeveloperOptionsScreen */, 38A1C74493B816B8753F5BC2 /* LegalInformationScreen */, + 7B91CB64534AD870924CCFEF /* NotificationSettingsScreen */, B364E08924AD15820350CDD9 /* SettingsScreen */, ); path = Settings; @@ -2420,6 +2438,7 @@ F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */, 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */, 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */, + 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */, 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, @@ -2568,6 +2587,18 @@ path = View; sourceTree = ""; }; + 7B91CB64534AD870924CCFEF /* NotificationSettingsScreen */ = { + isa = PBXGroup; + children = ( + 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */, + 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */, + 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */, + 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */, + BE7641A284D3E81DC96943E3 /* View */, + ); + path = NotificationSettingsScreen; + sourceTree = ""; + }; 7DBC911559934065993A5FF4 /* NotificationManager */ = { isa = PBXGroup; children = ( @@ -2792,6 +2823,7 @@ 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */, 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */, 75910F5A36EA8FF9BAD08D18 /* MigrationScreenUITests.swift */, + B83BC0DC9A2DF2DD60F9B6E9 /* NotificationSettingsScreenUITests.swift */, 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */, 122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */, @@ -2970,6 +3002,14 @@ path = View; sourceTree = ""; }; + A9E072BD31B744FC34F4D368 /* Helper */ = { + isa = PBXGroup; + children = ( + 9E685274772980BDEFF6691E /* UNUserNotificationCenter+Settings.swift */, + ); + path = Helper; + sourceTree = ""; + }; AAFDD509929A0CCF8BCE51EB /* Authentication */ = { isa = PBXGroup; children = ( @@ -3131,6 +3171,14 @@ path = ServerConfirmationScreen; sourceTree = ""; }; + BE7641A284D3E81DC96943E3 /* View */ = { + isa = PBXGroup; + children = ( + C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */, + ); + path = View; + sourceTree = ""; + }; C0937E3B06A8F0E2DB7C8241 /* Other */ = { isa = PBXGroup; children = ( @@ -4037,6 +4085,7 @@ 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */, 4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */, 1B2DADC008EE211AF1DA5292 /* NotificationManagerTests.swift in Sources */, + E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */, F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */, @@ -4341,6 +4390,11 @@ CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */, 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */, 06D3942496E9E0E655F14D21 /* NotificationManagerProtocol.swift in Sources */, + 6786C4B0936AC84D993B20BF /* NotificationSettingsScreen.swift in Sources */, + F6DFA23885980118AD7359C5 /* NotificationSettingsScreenCoordinator.swift in Sources */, + D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */, + 7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */, + CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */, 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */, 329571083B132E4941131835 /* OnboardingBackgroundImage.swift in Sources */, 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */, @@ -4530,6 +4584,7 @@ 22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */, 706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */, D02AA6208C7ACB9BE6332394 /* UNNotificationContent.swift in Sources */, + 17780569FB41E9BAC60D4710 /* UNUserNotificationCenter+Settings.swift in Sources */, 071A017E415AD378F2961B11 /* URL.swift in Sources */, 90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */, 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */, @@ -4598,6 +4653,7 @@ 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */, 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */, 51C240F4660F7269203A9B3A /* MigrationScreenUITests.swift in Sources */, + AF4232E6F08C3DB86FFA9BBD /* NotificationSettingsScreenUITests.swift in Sources */, 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */, F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */, diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 798cf723d..c283063f7 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -133,12 +133,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, // MARK: - NotificationManagerDelegate - func authorizationStatusUpdated(_ service: NotificationManagerProtocol, granted: Bool) { - if granted { - UIApplication.shared.registerForRemoteNotifications() - } + func registerForRemoteNotifications() { + UIApplication.shared.registerForRemoteNotifications() } + func unregisterForRemoteNotifications() { + UIApplication.shared.unregisterForRemoteNotifications() + } + func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol, content: UNNotificationContent) -> Bool { guard let roomId = content.roomID else { return true diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index e0e4ac37e..ba9e83a62 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -26,6 +26,7 @@ final class AppSettings { case migratedAccounts case timelineStyle case analyticsConsentState + case enableNotifications case enableInAppNotifications case pusherProfileTag case shouldCollapseRoomStateEvents @@ -34,6 +35,7 @@ final class AppSettings { case locationEventsEnabled case shareLocationEnabled case hasShownWelcomeScreen + case notificationSettingsEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -178,6 +180,9 @@ final class AppSettings { // MARK: - Notifications + @UserPreference(key: UserDefaultsKeys.enableNotifications, defaultValue: true, storageType: .userDefaults(store)) + var enableNotifications + @UserPreference(key: UserDefaultsKeys.enableInAppNotifications, defaultValue: true, storageType: .userDefaults(store)) var enableInAppNotifications @@ -218,4 +223,7 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.shareLocationEnabled, defaultValue: false, storageType: .userDefaults(store)) var shareLocationEnabled + + @UserPreference(key: UserDefaultsKeys.notificationSettingsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var notificationSettingsEnabled } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 42b00923d..07b2a96ec 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1317,4 +1317,86 @@ class UserIndicatorControllerMock: UserIndicatorControllerProtocol { } } } +class UserNotificationCenterMock: UserNotificationCenterProtocol { + var delegate: UNUserNotificationCenterDelegate? + + //MARK: - add + + var addThrowableError: Error? + var addCallsCount = 0 + var addCalled: Bool { + return addCallsCount > 0 + } + var addReceivedRequest: UNNotificationRequest? + var addReceivedInvocations: [UNNotificationRequest] = [] + var addClosure: ((UNNotificationRequest) async throws -> Void)? + + func add(_ request: UNNotificationRequest) async throws { + if let error = addThrowableError { + throw error + } + addCallsCount += 1 + addReceivedRequest = request + addReceivedInvocations.append(request) + try await addClosure?(request) + } + //MARK: - requestAuthorization + + var requestAuthorizationOptionsThrowableError: Error? + var requestAuthorizationOptionsCallsCount = 0 + var requestAuthorizationOptionsCalled: Bool { + return requestAuthorizationOptionsCallsCount > 0 + } + var requestAuthorizationOptionsReceivedOptions: UNAuthorizationOptions? + var requestAuthorizationOptionsReceivedInvocations: [UNAuthorizationOptions] = [] + var requestAuthorizationOptionsReturnValue: Bool! + var requestAuthorizationOptionsClosure: ((UNAuthorizationOptions) async throws -> Bool)? + + func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool { + if let error = requestAuthorizationOptionsThrowableError { + throw error + } + requestAuthorizationOptionsCallsCount += 1 + requestAuthorizationOptionsReceivedOptions = options + requestAuthorizationOptionsReceivedInvocations.append(options) + if let requestAuthorizationOptionsClosure = requestAuthorizationOptionsClosure { + return try await requestAuthorizationOptionsClosure(options) + } else { + return requestAuthorizationOptionsReturnValue + } + } + //MARK: - setNotificationCategories + + var setNotificationCategoriesCallsCount = 0 + var setNotificationCategoriesCalled: Bool { + return setNotificationCategoriesCallsCount > 0 + } + var setNotificationCategoriesReceivedCategories: Set? + var setNotificationCategoriesReceivedInvocations: [Set] = [] + var setNotificationCategoriesClosure: ((Set) -> Void)? + + func setNotificationCategories(_ categories: Set) { + setNotificationCategoriesCallsCount += 1 + setNotificationCategoriesReceivedCategories = categories + setNotificationCategoriesReceivedInvocations.append(categories) + setNotificationCategoriesClosure?(categories) + } + //MARK: - authorizationStatus + + var authorizationStatusCallsCount = 0 + var authorizationStatusCalled: Bool { + return authorizationStatusCallsCount > 0 + } + var authorizationStatusReturnValue: UNAuthorizationStatus! + var authorizationStatusClosure: (() async -> UNAuthorizationStatus)? + + func authorizationStatus() async -> UNAuthorizationStatus { + authorizationStatusCallsCount += 1 + if let authorizationStatusClosure = authorizationStatusClosure { + return await authorizationStatusClosure() + } else { + return authorizationStatusReturnValue + } + } +} // swiftlint:enable all diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 66927fa8b..f9f615104 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -31,6 +31,7 @@ struct DeveloperOptionsScreenViewStateBindings { var isEncryptionSyncEnabled: Bool var locationEventsEnabled: Bool var shareLocationEnabled: Bool + var notificationSettingsEnabled: Bool } enum DeveloperOptionsScreenViewAction { @@ -40,5 +41,6 @@ enum DeveloperOptionsScreenViewAction { case changedIsEncryptionSyncEnabled case changedLocationEventsEnabled case changedShareLocationEnabled + case changedNotificationSettingsEnabled case clearCache } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 23fcf059d..33796519f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -31,7 +31,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve readReceiptsEnabled: appSettings.readReceiptsEnabled, isEncryptionSyncEnabled: appSettings.isEncryptionSyncEnabled, locationEventsEnabled: appSettings.locationEventsEnabled, - shareLocationEnabled: appSettings.shareLocationEnabled) + shareLocationEnabled: appSettings.shareLocationEnabled, + notificationSettingsEnabled: appSettings.notificationSettingsEnabled) let state = DeveloperOptionsScreenViewState(bindings: bindings) super.init(initialViewState: state) @@ -55,6 +56,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve appSettings.locationEventsEnabled = state.bindings.locationEventsEnabled case .changedShareLocationEnabled: appSettings.shareLocationEnabled = state.bindings.shareLocationEnabled + case .changedNotificationSettingsEnabled: + appSettings.notificationSettingsEnabled = state.bindings.notificationSettingsEnabled case .clearCache: callback?(.clearCache) } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index b8e0935a4..b31bc45f1 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -66,6 +66,13 @@ struct DeveloperOptionsScreen: View { .onChange(of: context.shareLocationEnabled) { _ in context.send(viewAction: .changedShareLocationEnabled) } + + Toggle(isOn: $context.notificationSettingsEnabled) { + Text("Show notification settings") + } + .onChange(of: context.notificationSettingsEnabled) { _ in + context.send(viewAction: .changedNotificationSettingsEnabled) + } } Section { diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift new file mode 100644 index 000000000..cad158c0b --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -0,0 +1,48 @@ +// +// Copyright 2022 New Vector Ltd +// +// 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 Combine +import SwiftUI + +struct NotificationSettingsScreenCoordinatorParameters { + let userNotificationCenter: UserNotificationCenterProtocol +} + +enum NotificationSettingsScreenCoordinatorAction { } + +final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { + private let parameters: NotificationSettingsScreenCoordinatorParameters + private var viewModel: NotificationSettingsScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables: Set = .init() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: NotificationSettingsScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = NotificationSettingsScreenViewModel(appSettings: ServiceLocator.shared.settings, + userNotificationCenter: parameters.userNotificationCenter) + } + + func start() { } + + func toPresentable() -> AnyView { + AnyView(NotificationSettingsScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift new file mode 100644 index 000000000..00534ede4 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -0,0 +1,53 @@ +// +// Copyright 2022 New Vector Ltd +// +// 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 UIKit + +enum NotificationSettingsScreenViewModelAction { } + +struct NotificationSettingsScreenViewState: BindableState { + var bindings: NotificationSettingsScreenViewStateBindings + var strings = NotificationSettingsScreenStrings() + var isUserPermissionGranted: Bool? + + var showSystemNotificationsAlert: Bool { + bindings.enableNotifications && isUserPermissionGranted == false + } +} + +struct NotificationSettingsScreenViewStateBindings { + var enableNotifications = false +} + +struct NotificationSettingsScreenStrings { + let changeYourSystemSettings: AttributedString = { + let linkPlaceholder = "{link}" + var text = AttributedString(L10n.screenNotificationSettingsSystemNotificationsActionRequired(linkPlaceholder)) + var linkString = AttributedString(L10n.screenNotificationSettingsSystemNotificationsActionRequiredContentLink) + // Note: On the simulator, `UIApplication.openNotificationSettingsURLString` opens the `Settings` application instead of the application's notification settings screen. + linkString.link = URL(string: UIApplication.openNotificationSettingsURLString) + linkString.bold() + text.replace(linkPlaceholder, with: linkString) + + return text + }() +} + +enum NotificationSettingsScreenViewAction { + case linkClicked(url: URL) + case changedEnableNotifications +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift new file mode 100644 index 000000000..61fd96dae --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -0,0 +1,76 @@ +// +// Copyright 2022 New Vector Ltd +// +// 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 Combine +import SwiftUI + +typealias NotificationSettingsScreenViewModelType = StateStoreViewModel + +class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelType, NotificationSettingsScreenViewModelProtocol { + private var actionsSubject: PassthroughSubject = .init() + private let appSettings: AppSettings + private let userNotificationCenter: UserNotificationCenterProtocol + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol) { + self.appSettings = appSettings + self.userNotificationCenter = userNotificationCenter + let bindings = NotificationSettingsScreenViewStateBindings(enableNotifications: appSettings.enableNotifications) + super.init(initialViewState: NotificationSettingsScreenViewState(bindings: bindings)) + + // Listen for changes to AppSettings.enableNotifications + appSettings.$enableNotifications + .weakAssign(to: \.state.bindings.enableNotifications, on: self) + .store(in: &cancellables) + + // Refresh authorization status uppon UIApplication.didBecomeActiveNotification notification + NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) + .sink { [weak self] _ in + Task { + await self?.readSystemAuthorizationStatus() + } + } + .store(in: &cancellables) + + Task { + await readSystemAuthorizationStatus() + } + } + + // MARK: - Public + + override func process(viewAction: NotificationSettingsScreenViewAction) { + switch viewAction { + case .linkClicked(let url): + MXLog.warning("Link clicked: \(url)") + case .changedEnableNotifications: + toggleNotifications() + } + } + + // MARK: - Private + + func readSystemAuthorizationStatus() async { + state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized + } + + func toggleNotifications() { + appSettings.enableNotifications.toggle() + } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModelProtocol.swift new file mode 100644 index 000000000..9694cf1cc --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// 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 Combine + +@MainActor +protocol NotificationSettingsScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: NotificationSettingsScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift new file mode 100644 index 000000000..01dc2dc83 --- /dev/null +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -0,0 +1,88 @@ +// +// Copyright 2022 New Vector Ltd +// +// 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 SwiftUI + +struct NotificationSettingsScreen: View { + @ObservedObject var context: NotificationSettingsScreenViewModel.Context + + var body: some View { + Form { + if context.viewState.showSystemNotificationsAlert { + userPermissionSection + } + enableNotificationSection + } + .compoundForm() + .navigationTitle(L10n.screenNotificationSettingsTitle) + .track(screen: .settingsNotifications) + } + + // MARK: - Private + + private var userPermissionSection: some View { + Section { + HStack(alignment: .firstTextBaseline, spacing: 13) { + Image(systemName: "exclamationmark.circle.fill") + .foregroundColor(.compound.iconTertiaryAlpha) + VStack(alignment: .leading, spacing: 2) { + Text(L10n.screenNotificationSettingsSystemNotificationsTurnedOff) + .font(.compound.bodyLG) + .foregroundColor(.compound.textPrimary) + Text(context.viewState.strings.changeYourSystemSettings) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + .tint(.compound.textPrimary) + .environment(\.openURL, OpenURLAction { url in + context.send(viewAction: .linkClicked(url: url)) + return .systemAction + }) + } + .padding(.vertical, 5) + } + } + .compoundFormSection() + } + + private var enableNotificationSection: some View { + Section { + Toggle(isOn: $context.enableNotifications) { + Text(L10n.screenNotificationSettingsEnableNotifications) + } + .toggleStyle(.compoundForm) + .onChange(of: context.enableNotifications) { _ in + context.send(viewAction: .changedEnableNotifications) + } + } + .compoundFormSection() + } +} + +// MARK: - Previews + +struct NotificationSettingsScreen_Previews: PreviewProvider { + static let viewModel: NotificationSettingsScreenViewModel = { + let appSettings = AppSettings() + let notificationCenter = UserNotificationCenterMock() + notificationCenter.authorizationStatusReturnValue = .notDetermined + var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: notificationCenter) + return viewModel + }() + + static var previews: some View { + NotificationSettingsScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index f63a8964b..e0fa885ca 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -59,6 +59,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { self.presentDeveloperOptions() case .logout: self.callback?(.logout) + case .notifications: + self.presentNotificationSettings() } } } @@ -136,4 +138,12 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { private func showSuccess(label: String) { parameters.userIndicatorController?.submitIndicator(UserIndicator(title: label)) } + + private func presentNotificationSettings() { + let notificationParameters = NotificationSettingsScreenCoordinatorParameters( + userNotificationCenter: UNUserNotificationCenter.current() + ) + let coordinator = NotificationSettingsScreenCoordinator(parameters: notificationParameters) + parameters.navigationStackCoordinator?.push(coordinator) + } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 480a789bd..74096f40f 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -24,6 +24,7 @@ enum SettingsScreenViewModelAction { case about case sessionVerification case developerOptions + case notifications case logout } @@ -34,6 +35,7 @@ struct SettingsScreenViewState: BindableState { var userAvatarURL: URL? var userDisplayName: String? var showSessionVerificationSection: Bool + var showNotificationSettings: Bool var showDeveloperOptions: Bool } @@ -50,4 +52,5 @@ enum SettingsScreenViewAction { case logout case changedTimelineStyle case developerOptions + case notifications } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 7367d298c..cfcb71bec 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -40,6 +40,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo deviceID: userSession.deviceID, userID: userSession.userID, showSessionVerificationSection: showSessionVerificationSection, + showNotificationSettings: appSettings.notificationSettingsEnabled, showDeveloperOptions: appSettings.canShowDeveloperOptions), imageProvider: userSession.mediaProvider) @@ -51,6 +52,10 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .weakAssign(to: \.state.userAvatarURL, on: self) .store(in: &cancellables) + appSettings.$notificationSettingsEnabled + .weakAssign(to: \.state.showNotificationSettings, on: self) + .store(in: &cancellables) + Task { await userSession.clientProxy.loadUserAvatarURL() } @@ -92,6 +97,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo callback?(.sessionVerification) case .changedTimelineStyle: appSettings.timelineStyle = state.bindings.timelineStyle + case .notifications: + callback?(.notifications) case .developerOptions: callback?(.developerOptions) } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 4b1b4875c..0430cf4c9 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -113,6 +113,15 @@ struct SettingsScreen: View { context.send(viewAction: .changedTimelineStyle) } + // Notifications + if context.viewState.showNotificationSettings { + Button { context.send(viewAction: .notifications) } label: { + Label(L10n.screenNotificationSettingsTitle, systemImage: "bell") + } + .buttonStyle(.compoundForm(accessory: .navigationLink)) + .accessibilityIdentifier("notificationsButton") + } + // Analytics Button { context.send(viewAction: .analytics) } label: { Label(L10n.commonAnalytics, systemImage: "chart.bar") @@ -194,6 +203,7 @@ struct SettingsScreen_Previews: PreviewProvider { static let viewModel = { let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com"), mediaProvider: MockMediaProvider()) + ServiceLocator.shared.settings.notificationSettingsEnabled = true return SettingsScreenViewModel(userSession: userSession, appSettings: ServiceLocator.shared.settings) }() diff --git a/ElementX/Sources/Services/Notification/Helper/UNUserNotificationCenter+Settings.swift b/ElementX/Sources/Services/Notification/Helper/UNUserNotificationCenter+Settings.swift new file mode 100644 index 000000000..939c20454 --- /dev/null +++ b/ElementX/Sources/Services/Notification/Helper/UNUserNotificationCenter+Settings.swift @@ -0,0 +1,24 @@ +// +// Copyright 2023 New Vector Ltd +// +// 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 UserNotifications + +extension UNUserNotificationCenter { + func authorizationStatus() async -> UNAuthorizationStatus { + await notificationSettings().authorizationStatus + } +} diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index db6ae3196..6a6d85bde 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -24,7 +24,9 @@ class NotificationManager: NSObject, NotificationManagerProtocol { private let appSettings: AppSettings private var userSession: UserSessionProtocol? - + private var cancellables = Set() + private var notificationsEnabled = false + init(notificationCenter: UserNotificationCenterProtocol, appSettings: AppSettings) { self.notificationCenter = notificationCenter @@ -50,15 +52,35 @@ class NotificationManager: NSObject, NotificationManagerProtocol { options: []) notificationCenter.setNotificationCategories([messageCategory, inviteCategory]) notificationCenter.delegate = self + + notificationsEnabled = appSettings.enableNotifications + MXLog.info("[NotificationManager] app setting 'enableNotifications' is '\(notificationsEnabled)'") + + // Listen for changes to AppSettings.enableNotifications + appSettings.$enableNotifications + .sink { [weak self] newValue in + self?.enableNotifications(newValue) + } + .store(in: &cancellables) + + // Request authorization uppon UIApplication.didBecomeActiveNotification notification + NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) + .sink { [weak self] _ in + self?.requestAuthorization() + } + .store(in: &cancellables) } - + func requestAuthorization() { + guard appSettings.enableNotifications, !userSession.isNil else { return } Task { do { - let granted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) - MXLog.info("[NotificationManager] permission granted: \(granted)") + let permissionGranted = try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) + MXLog.info("[NotificationManager] permission granted: \(permissionGranted)") await MainActor.run { - self.delegate?.authorizationStatusUpdated(self, granted: granted) + if permissionGranted { + self.delegate?.registerForRemoteNotifications() + } } } catch { MXLog.error("[NotificationManager] request authorization failed: \(error)") @@ -136,6 +158,17 @@ class NotificationManager: NSObject, NotificationManagerProtocol { appSettings.pusherProfileTag = newTag return newTag } + + private func enableNotifications(_ enable: Bool) { + guard notificationsEnabled != enable else { return } + notificationsEnabled = enable + MXLog.info("[NotificationManager] app setting 'enableNotifications' changed to '\(enable)'") + if enable { + requestAuthorization() + } else { + delegate?.unregisterForRemoteNotifications() + } + } } // MARK: - UNUserNotificationCenterDelegate diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift index a74f73508..306fb01d4 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -18,8 +18,6 @@ import Foundation import UserNotifications protocol NotificationManagerDelegate: AnyObject { - func authorizationStatusUpdated(_ service: NotificationManagerProtocol, - granted: Bool) func shouldDisplayInAppNotification(_ service: NotificationManagerProtocol, content: UNNotificationContent) -> Bool func notificationTapped(_ service: NotificationManagerProtocol, @@ -27,6 +25,8 @@ protocol NotificationManagerDelegate: AnyObject { func handleInlineReply(_ service: NotificationManagerProtocol, content: UNNotificationContent, replyText: String) async + func registerForRemoteNotifications() + func unregisterForRemoteNotifications() } // MARK: - NotificationManagerProtocol diff --git a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift index ea42b1e26..40e46a610 100644 --- a/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/UserNotificationCenterProtocol.swift @@ -22,4 +22,8 @@ protocol UserNotificationCenterProtocol: AnyObject { func add(_ request: UNNotificationRequest) async throws func requestAuthorization(options: UNAuthorizationOptions) async throws -> Bool func setNotificationCategories(_ categories: Set) + func authorizationStatus() async -> UNAuthorizationStatus } + +// sourcery: AutoMockable +extension UserNotificationCenterProtocol { } diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 6ed69c730..b41c11b70 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -191,6 +191,11 @@ class MockScreen: Identifiable { isModallyPresented: false)) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .notificationSettingsScreen: + let userNotificationCenter = UserNotificationCenterMock() + userNotificationCenter.authorizationStatusReturnValue = .denied + let parameters = NotificationSettingsScreenCoordinatorParameters(userNotificationCenter: userNotificationCenter) + return NotificationSettingsScreenCoordinator(parameters: parameters) case .onboarding: return OnboardingCoordinator() case .roomPlainNoAvatar: diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 668eb20dc..4fd9bfbb9 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -34,6 +34,7 @@ enum UITestsScreenIdentifier: String { case settings case bugReport case bugReportWithScreenshot + case notificationSettingsScreen case onboarding case roomPlainNoAvatar case roomEncryptedWithAvatar diff --git a/UITests/Sources/NotificationSettingsScreenUITests.swift b/UITests/Sources/NotificationSettingsScreenUITests.swift new file mode 100644 index 000000000..99a434eed --- /dev/null +++ b/UITests/Sources/NotificationSettingsScreenUITests.swift @@ -0,0 +1,21 @@ +// +// Copyright 2023 New Vector Ltd +// +// 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 ElementX +import XCTest + +@MainActor +class NotificationSettingsScreenUITests: XCTestCase { } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 04e03e2bf..f9a320147 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -179,8 +179,12 @@ final class NotificationManagerTests: XCTestCase { } extension NotificationManagerTests: NotificationManagerDelegate { - func authorizationStatusUpdated(_ service: ElementX.NotificationManagerProtocol, granted: Bool) { - authorizationStatusWasGranted = granted + func registerForRemoteNotifications() { + authorizationStatusWasGranted = true + } + + func unregisterForRemoteNotifications() { + authorizationStatusWasGranted = false } func shouldDisplayInAppNotification(_ service: ElementX.NotificationManagerProtocol, content: UNNotificationContent) -> Bool { diff --git a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift index cc103412b..84a62bfd6 100644 --- a/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift +++ b/UnitTests/Sources/NotificationManager/UserNotificationCenterSpy.swift @@ -36,4 +36,9 @@ class UserNotificationCenterSpy: UserNotificationCenterProtocol { func setNotificationCategories(_ categories: Set) { notificationCategoriesValue = categories } + + var authorizationStatusValue: UNAuthorizationStatus? + func authorizationStatus() async -> UNAuthorizationStatus { + authorizationStatusValue ?? UNAuthorizationStatus.denied + } } diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift new file mode 100644 index 000000000..6daa5567f --- /dev/null +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -0,0 +1,50 @@ +// +// Copyright 2023 New Vector Ltd +// +// 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 XCTest + +@testable import ElementX + +@MainActor +class NotificationSettingsScreenViewModelTests: XCTestCase { + private var viewModel: NotificationSettingsScreenViewModelProtocol! + private var context: NotificationSettingsScreenViewModelType.Context! + private var appSettings: AppSettings! + private var userNotificationCenter: UserNotificationCenterMock! + + @MainActor override func setUpWithError() throws { + AppSettings.reset() + + userNotificationCenter = UserNotificationCenterMock() + userNotificationCenter.authorizationStatusReturnValue = .authorized + appSettings = AppSettings() + viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, + userNotificationCenter: userNotificationCenter) + context = viewModel.context + } + + func testEnableNotifications() { + appSettings.enableNotifications = false + context.send(viewAction: .changedEnableNotifications) + XCTAssertTrue(appSettings.enableNotifications) + } + + func testOptOut() { + appSettings.enableNotifications = true + context.send(viewAction: .changedEnableNotifications) + XCTAssertFalse(appSettings.enableNotifications) + } +}