diff --git a/src/xcode/ENA/ENA.xcodeproj/project.pbxproj b/src/xcode/ENA/ENA.xcodeproj/project.pbxproj index 2fcae8965da..41969614911 100644 --- a/src/xcode/ENA/ENA.xcodeproj/project.pbxproj +++ b/src/xcode/ENA/ENA.xcodeproj/project.pbxproj @@ -322,6 +322,7 @@ 505F508325C897B3004920EB /* OTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505F508225C897B3004920EB /* OTPError.swift */; }; 506A2F1B25D2A06D00B7DDFB /* DataDonationDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506A2F1A25D2A06D00B7DDFB /* DataDonationDetailsViewModel.swift */; }; 506A2F2025D2A08900B7DDFB /* DataDonationDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506A2F1F25D2A08900B7DDFB /* DataDonationDetailsViewController.swift */; }; + 508DB7D525D6B49E00F153D6 /* DataDonationDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508DB7D425D6B49E00F153D6 /* DataDonationDetailsViewModelTests.swift */; }; 509C69FE25B5D920000F2A4C /* ENAUITests-Statistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509C69FD25B5D920000F2A4C /* ENAUITests-Statistics.swift */; }; 50B1D6E72551621C00684C3C /* DayKeyPackageDownloadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B1D6E62551621C00684C3C /* DayKeyPackageDownloadTests.swift */; }; 50B5057D25CAEF5C00EEA380 /* OTPToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5057C25CAEF5C00EEA380 /* OTPToken.swift */; }; @@ -614,7 +615,7 @@ B1FF6B6E2497D0B50041CF02 /* CWASQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = B1FF6B6C2497D0B50041CF02 /* CWASQLite.h */; settings = {ATTRIBUTES = (Public, ); }; }; B1FF6B772497D2330041CF02 /* sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DFCC2712484DC8400E2811D /* sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; BA0073BE25D18A5B0036DFDD /* DataDonationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0073BD25D18A5B0036DFDD /* DataDonationViewController.swift */; }; - BA0073EA25D196900036DFDD /* DataDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0073E925D196900036DFDD /* DataDonationViewModel.swift */; }; + BA0073EA25D196900036DFDD /* DefaultDataDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0073E925D196900036DFDD /* DefaultDataDonationViewModel.swift */; }; BA00740025D1A1810036DFDD /* District.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0073FF25D1A1810036DFDD /* District.swift */; }; BA056F1B259B89B50022B0A4 /* RiskLegendDotBodyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA056F19259B89B50022B0A4 /* RiskLegendDotBodyCell.swift */; }; BA07735C25C2FCB800EAF6B8 /* PPACService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA07735B25C2FCB800EAF6B8 /* PPACService.swift */; }; @@ -654,7 +655,8 @@ BA7D177925D547D4006B9EBF /* DataDonationModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7D177825D547D4006B9EBF /* DataDonationModelTests.swift */; }; BA7D178125D549B3006B9EBF /* testData.json in Resources */ = {isa = PBXBuildFile; fileRef = BA7D178025D549AA006B9EBF /* testData.json */; }; BA7D178925D5590C006B9EBF /* testDataInvalid.json in Resources */ = {isa = PBXBuildFile; fileRef = BA7D178825D558FD006B9EBF /* testDataInvalid.json */; }; - BA7D178E25D55EB1006B9EBF /* DataDonationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7D178D25D55EB1006B9EBF /* DataDonationViewModelTests.swift */; }; + BA7D178E25D55EB1006B9EBF /* DefaultDataDonationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7D178D25D55EB1006B9EBF /* DefaultDataDonationViewModelTests.swift */; }; + BA7D17B725D59373006B9EBF /* SettingsDataDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7D17B625D59373006B9EBF /* SettingsDataDonationViewModel.swift */; }; BA7EABAD25C973FE001AA5FE /* DMKeyValueCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA7EABAC25C973FE001AA5FE /* DMKeyValueCellViewModel.swift */; }; BA811B4D25D422B600C9BC98 /* DataDonationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA811B4C25D422B600C9BC98 /* DataDonationModel.swift */; }; BA8824D725D2A88800AD9B11 /* ppdd-ppa-administrative-unit-set-ua-approved.json in Resources */ = {isa = PBXBuildFile; fileRef = BA8824D625D2A88800AD9B11 /* ppdd-ppa-administrative-unit-set-ua-approved.json */; }; @@ -683,6 +685,10 @@ BACD671725B7002F00BAF5D0 /* RiskCalculationResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BACD671625B7002F00BAF5D0 /* RiskCalculationResultTests.swift */; }; BAD962FB25668F4000FAB615 /* TestResultAvailableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD962FA25668F4000FAB615 /* TestResultAvailableViewController.swift */; }; BAD9630025668F8E00FAB615 /* TestResultAvailableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAD962FF25668F8E00FAB615 /* TestResultAvailableViewModel.swift */; }; + BADBC81B25D6A7FF00488DB7 /* BaseDataDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADBC81A25D6A7FF00488DB7 /* BaseDataDonationViewModel.swift */; }; + BADBC82925D6BB6400488DB7 /* DistrictTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADBC82825D6BB6400488DB7 /* DistrictTests.swift */; }; + BADBC85625D6C1C500488DB7 /* BaseDataDonationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADBC85525D6C1C500488DB7 /* BaseDataDonationViewModelTests.swift */; }; + BADBC86725D6C7FD00488DB7 /* SettingsDataDonationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADBC86625D6C7FD00488DB7 /* SettingsDataDonationViewModelTests.swift */; }; BAE20FF325C8321300162966 /* DMKeyValueTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE20FF125C8321300162966 /* DMKeyValueTableViewCell.swift */; }; BAE2101025C8460900162966 /* PPACDeviceCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE2100125C83E4400162966 /* PPACDeviceCheck.swift */; }; BAE2101B25C8463400162966 /* PPACDeviceCheckMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE2101A25C8463400162966 /* PPACDeviceCheckMock.swift */; }; @@ -1124,6 +1130,7 @@ 505F508225C897B3004920EB /* OTPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPError.swift; sourceTree = ""; }; 506A2F1A25D2A06D00B7DDFB /* DataDonationDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationDetailsViewModel.swift; sourceTree = ""; }; 506A2F1F25D2A08900B7DDFB /* DataDonationDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationDetailsViewController.swift; sourceTree = ""; }; + 508DB7D425D6B49E00F153D6 /* DataDonationDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationDetailsViewModelTests.swift; sourceTree = ""; }; 509C69FD25B5D920000F2A4C /* ENAUITests-Statistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ENAUITests-Statistics.swift"; sourceTree = ""; }; 50B1D6E62551621C00684C3C /* DayKeyPackageDownloadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayKeyPackageDownloadTests.swift; sourceTree = ""; }; 50B5057C25CAEF5C00EEA380 /* OTPToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPToken.swift; sourceTree = ""; }; @@ -1431,7 +1438,7 @@ B1FF6B6C2497D0B50041CF02 /* CWASQLite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CWASQLite.h; sourceTree = ""; }; B1FF6B6D2497D0B50041CF02 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; BA0073BD25D18A5B0036DFDD /* DataDonationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationViewController.swift; sourceTree = ""; }; - BA0073E925D196900036DFDD /* DataDonationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationViewModel.swift; sourceTree = ""; }; + BA0073E925D196900036DFDD /* DefaultDataDonationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDataDonationViewModel.swift; sourceTree = ""; }; BA0073FF25D1A1810036DFDD /* District.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = District.swift; sourceTree = ""; }; BA056F19259B89B50022B0A4 /* RiskLegendDotBodyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiskLegendDotBodyCell.swift; sourceTree = ""; }; BA07735B25C2FCB800EAF6B8 /* PPACService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPACService.swift; sourceTree = ""; }; @@ -1472,7 +1479,8 @@ BA7D177825D547D4006B9EBF /* DataDonationModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationModelTests.swift; sourceTree = ""; }; BA7D178025D549AA006B9EBF /* testData.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = testData.json; sourceTree = ""; }; BA7D178825D558FD006B9EBF /* testDataInvalid.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = testDataInvalid.json; sourceTree = ""; }; - BA7D178D25D55EB1006B9EBF /* DataDonationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationViewModelTests.swift; sourceTree = ""; }; + BA7D178D25D55EB1006B9EBF /* DefaultDataDonationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultDataDonationViewModelTests.swift; sourceTree = ""; }; + BA7D17B625D59373006B9EBF /* SettingsDataDonationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataDonationViewModel.swift; sourceTree = ""; }; BA7EABAC25C973FE001AA5FE /* DMKeyValueCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMKeyValueCellViewModel.swift; sourceTree = ""; }; BA811B4C25D422B600C9BC98 /* DataDonationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataDonationModel.swift; sourceTree = ""; }; BA8824D625D2A88800AD9B11 /* ppdd-ppa-administrative-unit-set-ua-approved.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "ppdd-ppa-administrative-unit-set-ua-approved.json"; sourceTree = ""; }; @@ -1500,6 +1508,10 @@ BACD671625B7002F00BAF5D0 /* RiskCalculationResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiskCalculationResultTests.swift; sourceTree = ""; }; BAD962FA25668F4000FAB615 /* TestResultAvailableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestResultAvailableViewController.swift; sourceTree = ""; }; BAD962FF25668F8E00FAB615 /* TestResultAvailableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestResultAvailableViewModel.swift; sourceTree = ""; }; + BADBC81A25D6A7FF00488DB7 /* BaseDataDonationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDataDonationViewModel.swift; sourceTree = ""; }; + BADBC82825D6BB6400488DB7 /* DistrictTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistrictTests.swift; sourceTree = ""; }; + BADBC85525D6C1C500488DB7 /* BaseDataDonationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDataDonationViewModelTests.swift; sourceTree = ""; }; + BADBC86625D6C7FD00488DB7 /* SettingsDataDonationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDataDonationViewModelTests.swift; sourceTree = ""; }; BAE20FF125C8321300162966 /* DMKeyValueTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMKeyValueTableViewCell.swift; sourceTree = ""; }; BAE2100125C83E4400162966 /* PPACDeviceCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPACDeviceCheck.swift; sourceTree = ""; }; BAE2101A25C8463400162966 /* PPACDeviceCheckMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPACDeviceCheckMock.swift; sourceTree = ""; }; @@ -2453,6 +2465,14 @@ path = __tests__; sourceTree = ""; }; + 508DB7D925D6B4AE00F153D6 /* __tests__ */ = { + isa = PBXGroup; + children = ( + 508DB7D425D6B49E00F153D6 /* DataDonationDetailsViewModelTests.swift */, + ); + path = __tests__; + sourceTree = ""; + }; 50B5058725CAF3C500EEA380 /* DMOTPService */ = { isa = PBXGroup; children = ( @@ -2880,10 +2900,11 @@ 85D759712457059A008175F0 /* Scenes */ = { isa = PBXGroup; children = ( - BA0073BB25D189E50036DFDD /* Datadonation */, 71176E30248957B1004B0C9F /* App */, EE85998B2462EFD4002E7AE2 /* AppInformation */, 0190982A257E5A9D0065D050 /* ContactDiary */, + BA0073BB25D189E50036DFDD /* DataDonation */, + BA7D17D025D5947B006B9EBF /* DataDonationDetails */, 71F76D0E24767AF100515A01 /* DynamicTableViewController */, 858F6F71245AEC05009FFD33 /* ENSetting */, 514E81312461946E00636861 /* ExposureDetection */, @@ -2893,9 +2914,9 @@ 7143D07424990A3100608DDE /* NavigationControllerWithFooter */, 51D420AF2458308400AD70CA /* Onboarding */, 01B7232524F8128B0064C0EB /* OptionGroup */, - BA69A7D625CD782900023265 /* SelectValueViewController */, 13091950247972CF0066E329 /* PrivacyProtectionViewController */, EE20EA0824699A3A00770683 /* RiskLegend */, + BA69A7D625CD782900023265 /* SelectValueViewController */, 51D420C224583D7B00AD70CA /* Settings */, 016E260C25AF43440077C64C /* StatisticsInfo */, EBB92C70259E10BD00013B41 /* UpdateOS */, @@ -3826,18 +3847,14 @@ path = CWASQLite; sourceTree = ""; }; - BA0073BB25D189E50036DFDD /* Datadonation */ = { + BA0073BB25D189E50036DFDD /* DataDonation */ = { isa = PBXGroup; children = ( - BA7D177725D547BE006B9EBF /* __tests__ */, - 506A2F1F25D2A08900B7DDFB /* DataDonationDetailsViewController.swift */, - 506A2F1A25D2A06D00B7DDFB /* DataDonationDetailsViewModel.swift */, - BA811B4C25D422B600C9BC98 /* DataDonationModel.swift */, + BA7D17CC25D59465006B9EBF /* Model */, BA0073BD25D18A5B0036DFDD /* DataDonationViewController.swift */, - BA0073E925D196900036DFDD /* DataDonationViewModel.swift */, - BA0073FF25D1A1810036DFDD /* District.swift */, + BA7D17CB25D59440006B9EBF /* DataDonationViewModels */, ); - path = Datadonation; + path = DataDonation; sourceTree = ""; }; BA07735A25C2FC5E00EAF6B8 /* PPAccessControl */ = { @@ -4006,12 +4023,44 @@ children = ( BA7D178025D549AA006B9EBF /* testData.json */, BA7D178825D558FD006B9EBF /* testDataInvalid.json */, - BA7D177825D547D4006B9EBF /* DataDonationModelTests.swift */, - BA7D178D25D55EB1006B9EBF /* DataDonationViewModelTests.swift */, + BADBC85525D6C1C500488DB7 /* BaseDataDonationViewModelTests.swift */, + BA7D178D25D55EB1006B9EBF /* DefaultDataDonationViewModelTests.swift */, + BADBC86625D6C7FD00488DB7 /* SettingsDataDonationViewModelTests.swift */, ); path = __tests__; sourceTree = ""; }; + BA7D17CB25D59440006B9EBF /* DataDonationViewModels */ = { + isa = PBXGroup; + children = ( + BA7D177725D547BE006B9EBF /* __tests__ */, + BADBC81A25D6A7FF00488DB7 /* BaseDataDonationViewModel.swift */, + BA0073E925D196900036DFDD /* DefaultDataDonationViewModel.swift */, + BA7D17B625D59373006B9EBF /* SettingsDataDonationViewModel.swift */, + ); + path = DataDonationViewModels; + sourceTree = ""; + }; + BA7D17CC25D59465006B9EBF /* Model */ = { + isa = PBXGroup; + children = ( + BADBC82D25D6BD2800488DB7 /* __tests__ */, + BA811B4C25D422B600C9BC98 /* DataDonationModel.swift */, + BA0073FF25D1A1810036DFDD /* District.swift */, + ); + path = Model; + sourceTree = ""; + }; + BA7D17D025D5947B006B9EBF /* DataDonationDetails */ = { + isa = PBXGroup; + children = ( + 508DB7D925D6B4AE00F153D6 /* __tests__ */, + 506A2F1F25D2A08900B7DDFB /* DataDonationDetailsViewController.swift */, + 506A2F1A25D2A06D00B7DDFB /* DataDonationDetailsViewModel.swift */, + ); + path = DataDonationDetails; + sourceTree = ""; + }; BA9BCF7525B09B3A00DD7974 /* __tests__ */ = { isa = PBXGroup; children = ( @@ -4101,6 +4150,15 @@ path = TestResultAvailable; sourceTree = ""; }; + BADBC82D25D6BD2800488DB7 /* __tests__ */ = { + isa = PBXGroup; + children = ( + BA7D177825D547D4006B9EBF /* DataDonationModelTests.swift */, + BADBC82825D6BB6400488DB7 /* DistrictTests.swift */, + ); + path = __tests__; + sourceTree = ""; + }; BAEC99BE258B6FFA00B98ECA /* __tests__ */ = { isa = PBXGroup; children = ( @@ -4719,6 +4777,7 @@ ABDA2792251CE308006BAE84 /* DMServerEnvironmentViewController.swift in Sources */, 85E33444247EB357006E74EC /* CircularProgressView.swift in Sources */, AB1886D1252DE51E00D39BBE /* Bundle+Identifier.swift in Sources */, + BA7D17B725D59373006B9EBF /* SettingsDataDonationViewModel.swift in Sources */, 71FD8862246EB27F00E804D0 /* ExposureDetectionViewController.swift in Sources */, 013C413D255463A400826C9F /* DMDebugRiskCalculationViewController.swift in Sources */, BAB6C7EF25C4626100E042FB /* TimestampedToken.swift in Sources */, @@ -4777,7 +4836,7 @@ 51D420B724583B7200AD70CA /* NSObject+Identifier.swift in Sources */, BA69A7F825CD87C200023265 /* SelectValueCellViewModel.swift in Sources */, CDCE11D6247D644100F30825 /* NotificationSettingsViewModel.swift in Sources */, - BA0073EA25D196900036DFDD /* DataDonationViewModel.swift in Sources */, + BA0073EA25D196900036DFDD /* DefaultDataDonationViewModel.swift in Sources */, BA6C8AAE254D6476008344F5 /* ENARange.swift in Sources */, 50BD2E6424FE232E00932566 /* AppInformationImprintViewModel.swift in Sources */, 71330E4724810A0C00EB10F6 /* DynamicTableViewFooter.swift in Sources */, @@ -5091,6 +5150,7 @@ BA69A7D825CD787D00023265 /* SelectValueTableViewController.swift in Sources */, EBA403D12589260D00D1F039 /* ColorCompatibility.swift in Sources */, B161782524804AC3006E435A /* DownloadedPackagesSQLLiteStoreV1.swift in Sources */, + BADBC81B25D6A7FF00488DB7 /* BaseDataDonationViewModel.swift in Sources */, 01909888257E7B900065D050 /* ExposureSubmissionQRInfoViewController.swift in Sources */, 8F270194259B5BA700E48CFE /* ContactDiaryMigration1To2.swift in Sources */, BA112DF7255586E9007F5712 /* WifiOnlyHTTPClient.swift in Sources */, @@ -5132,6 +5192,7 @@ 01A1B467252E19D000841B63 /* ExposureSubmissionCoordinatorModelTests.swift in Sources */, B1EAEC8F247118D1003BE9A2 /* URLSession+ConvenienceTests.swift in Sources */, A32C046524D96348005BEA61 /* HTTPClient+PlausibeDeniabilityTests.swift in Sources */, + BADBC82925D6BB6400488DB7 /* DistrictTests.swift in Sources */, 0156F05C25D2DA2300543640 /* DeadmanNotificationManagerTests.swift in Sources */, A372DA4224BF3E29003248BB /* MockExposureSubmissionCoordinator.swift in Sources */, BA9BCF7725B09B5500DD7974 /* DiaryOverviewDayCellModelTests.swift in Sources */, @@ -5163,6 +5224,7 @@ BA9DD53F2567BDAC00C326FF /* TestResultAvailableViewModelTest.swift in Sources */, 01B605CE258A38330093DB8E /* DiaryEntryTest.swift in Sources */, 50352C8B25C94961007193D2 /* OTPServiceTests.swift in Sources */, + 508DB7D525D6B49E00F153D6 /* DataDonationDetailsViewModelTests.swift in Sources */, BAB6C81225C4652D00E042FB /* PPACTokenTests.swift in Sources */, F2DC808E248989CE00EDC40A /* DynamicTableViewControllerRegisterCellsTests.swift in Sources */, EE22DB91247FB479001B0A71 /* MockStateHandlerObserverDelegate.swift in Sources */, @@ -5182,6 +5244,7 @@ AB8BC34F2551BBE100F3B5A7 /* HourKeyPackagesDownloadTests.swift in Sources */, B1E23B8624FE4DD3006BCDA6 /* PublicKeyProviderTests.swift in Sources */, 01B605D9258A49E70093DB8E /* DiaryLocationTest.swift in Sources */, + BADBC86725D6C7FD00488DB7 /* SettingsDataDonationViewModelTests.swift in Sources */, F2DC809424898CE600EDC40A /* DynamicTableViewControllerFooterTests.swift in Sources */, 35358DD425A23169004FD0CB /* HTTPClientCertificatePinningTests.swift in Sources */, 01B72BA6258360FF00A3E3BC /* DiaryDayEmptyViewModelTest.swift in Sources */, @@ -5190,12 +5253,13 @@ B15382E7248290BB0010F007 /* AppleFilesWriterTests.swift in Sources */, AB1FCBD42521FC47005930BA /* ServerEnvironmentTests.swift in Sources */, 0123D5992501385200A91838 /* ExposureSubmissionErrorTests.swift in Sources */, - BA7D178E25D55EB1006B9EBF /* DataDonationViewModelTests.swift in Sources */, + BA7D178E25D55EB1006B9EBF /* DefaultDataDonationViewModelTests.swift in Sources */, 50C51CB625CDEA4300D4C33A /* HTTPClient+SubmitAnalyticsDataTests.swift in Sources */, 01A23685251A23740043D9F8 /* ExposureSubmissionQRInfoModelTests.swift in Sources */, 2FD881CC2490F65C00BEC8FC /* ExposureSubmissionHotlineViewControllerTest.swift in Sources */, 01A97DD12506768F00C07C37 /* DatePickerOptionViewModelTests.swift in Sources */, 0177F4B125503805009DD568 /* ScanInstanceTest.swift in Sources */, + BADBC85625D6C1C500488DB7 /* BaseDataDonationViewModelTests.swift in Sources */, B120C7C924AFE7B800F68FF1 /* ActiveTracingTests.swift in Sources */, 35AA4AF4259B40FC00D32306 /* CryptoFallbackTests.swift in Sources */, 01B72BC02583875600A3E3BC /* DiaryDayViewModelTest.swift in Sources */, diff --git a/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Contents.json b/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Contents.json new file mode 100644 index 00000000000..60cac38a707 --- /dev/null +++ b/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icons_Settings_Datenspende.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Icons_Settings_Datenspende.pdf b/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Icons_Settings_Datenspende.pdf new file mode 100644 index 00000000000..b5d0486ba7d Binary files /dev/null and b/src/xcode/ENA/ENA/Resources/Assets/Assets.xcassets/Common/Icons_Settings_Datenspende.imageset/Icons_Settings_Datenspende.pdf differ diff --git a/src/xcode/ENA/ENA/Resources/Localization/de.lproj/Localizable.legal.strings b/src/xcode/ENA/ENA/Resources/Localization/de.lproj/Localizable.legal.strings index 3860a0ef0b1..090120597b5 100644 --- a/src/xcode/ENA/ENA/Resources/Localization/de.lproj/Localizable.legal.strings +++ b/src/xcode/ENA/ENA/Resources/Localization/de.lproj/Localizable.legal.strings @@ -99,3 +99,7 @@ "SurveyConsent_Details_Legal_Body1" = "Um die Echtheit Ihrer App zu bestätigen, erzeugt Ihr Smartphone eine eindeutige Kennung, die Informationen über die Version Ihres Smartphones und der App enthält. Das ist erforderlich, um zu verhindern, dass Nutzer mehrfach der Befragung teilnehmen und so die Ergebnisse der Befragung verfälschen. Die Kennung wird hier einmalig an Apple übermittelt. Dabei kann es auch zu einer Datenübermittlung in die USA kommen. Dort besteht kein dem europäischen Recht angemessenes Datenschutzniveau und Ihre europäischen Datenschutzrechte können eventuell nicht durchgesetzt werden. Insbesondere besteht die Möglichkeit, dass US-Sicherheitsbehörden, auch ohne einen konkreten Verdacht, auf die übermittelten Daten bei Apple zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft nur die an Apple übermittelte Kennung. Die weiteren Angaben über Ihre Teilnahme an der Befragung erhält Apple nicht."; "SurveyConsent_Details_Legal_Body2" = "Wenn Sie mit der Drittlandsübermittlung nicht einverstanden sind, tippen Sie bitte nicht „Einverstanden“ an. Sie können die App weiterhin nutzen, eine Teilnahme an dieser Befragung ist dann jedoch nicht möglich."; + +/* Data Donation App Settings*/ + +"ppa_settings_privacy_information_body" = "Indem Sie oben „Datenspende“ aktivieren, willigen Sie ein:\n\nDie App übermittelt täglich von ihr erfasste Angaben an das RKI. Die Daten betreffen angezeigte Risiko-Begegnungen und Warnungen, durch Sie abgerufene Testergebnisse, ob Sie andere Nutzer gewarnt haben sowie Angaben über das Betriebssystem Ihres Smartphones. Wenn Sie oben weitere Angaben gemacht haben (Region, Altersgruppe), werden auch diese an das RKI übermittelt.\n\nDas RKI wird diese Daten zu Statistiken zusammenfassen und auswerten, um die Wirksamkeit und Funktionsweise der App zu bewerten und Rückschlüsse auf das Pandemiegeschehen zu ziehen. Die dabei gefundenen Erkenntnisse helfen bei der Verbesserung der Funktionen und Nutzerfreundlichkeit der App sowie bei der Steuerung anderer Maßnahmen der Pandemiebekämpfung.\n\nBevor Ihre Daten ausgewertet werden, muss sichergestellt sein, dass jede an der Datenspende teilnehmende App nur einmal gezählt wird und die Statistiken nicht verfälscht werden. Hierfür muss die Echtheit Ihrer App geprüft werden. Dazu wird durch Ihr Smartphone eine eindeutige Kennung erzeugt und an Apple in die USA übermittelt, damit Apple die Echtheit Ihrer App gegenüber dem RKI bestätigen kann. Die Kennung enthält Informationen über die Version Ihres Smartphones und der App. Weitere Angaben aus der App erhält Apple hierbei nicht.\n\nSie können Ihr Einverständnis jederzeit zurücknehmen, indem Sie oben „Datenspende“ deaktivieren."; diff --git a/src/xcode/ENA/ENA/Resources/Localization/en.lproj/Localizable.legal.strings b/src/xcode/ENA/ENA/Resources/Localization/en.lproj/Localizable.legal.strings index 1817b75a74d..ac2035fdf1d 100644 --- a/src/xcode/ENA/ENA/Resources/Localization/en.lproj/Localizable.legal.strings +++ b/src/xcode/ENA/ENA/Resources/Localization/en.lproj/Localizable.legal.strings @@ -75,7 +75,7 @@ "DataDonation_Acknowledgement_Title" = "Your Consent"; -"DataDonation_Acknowledgement_Content" = "By tapping on “Accept”, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Apple in the U.S., so that Apple can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. Apple does not receive any other information from the app during this process.\n\You can withdraw your consent at any time by disabling the data sharing feature in the app's settings."; +"DataDonation_Acknowledgement_Content" = "By tapping on “Accept”, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Apple in the U.S., so that Apple can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. Apple does not receive any other information from the app during this process.\n\nYou can withdraw your consent at any time by disabling the data sharing feature in the app's settings."; "DataDonation_Acknowledgement_BulletPoint_1" = "Your consent is voluntary. If you do not give your consent, you will still be able to use the app."; @@ -108,3 +108,7 @@ "SurveyConsent_Details_Legal_Body1" = "To confirm the authenticity of your app, your smartphone will generate a unique identifier that contains information about the version of your smartphone and the app. This is necessary to prevent users from participating in the survey more than once, as this could distort the results of the survey. Once generated, the identifier will be transmitted once to Apple. This may result in data being transferred to the U.S.. There, the level of data protection is not considered adequate under European law and it may not be possible to enforce your European data protection rights. In particular, there is a possibility that once the transmitted data reaches Apple, it may be accessed and analysed by U.S. security authorities, even if they have no specific grounds for suspicion, for example by linking the data with other information. This only concerns the identifier sent to Apple. Apple will not receive the other information about your participation in the survey."; "SurveyConsent_Details_Legal_Body2" = "If you do not consent to this transfer of your data to a third country, please do not tap on “Accept”. You will still be able to use the app, but not participate in this survey."; + +/* Data Donation App Settings*/ + +"ppa_settings_privacy_information_body" = "By enabling “Share Data” above, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Apple in the U.S., so that Apple can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. Apple does not receive any other information from the app during this process.\n\nYou can withdraw your consent at any time by disabling “Share Data” above."; diff --git a/src/xcode/ENA/ENA/Resources/Localization/tr.lproj/Localizable.legal.strings b/src/xcode/ENA/ENA/Resources/Localization/tr.lproj/Localizable.legal.strings index 49271a22388..71837bbe2b7 100644 --- a/src/xcode/ENA/ENA/Resources/Localization/tr.lproj/Localizable.legal.strings +++ b/src/xcode/ENA/ENA/Resources/Localization/tr.lproj/Localizable.legal.strings @@ -109,3 +109,7 @@ "SurveyConsent_Details_Legal_Body1" = "Uygulamanızın orijinal olduğunu onaylamak adına akıllı telefonunuz, akıllı telefonunuzun sürümü ve Uygulamaya ilişkin veriler içeren benzersiz bir kimlik kodu oluşturur. Kullanıcıların ankete birden çok kez katılmasını ve böylece anket sonuçlarını tahrif etmesini önlemek için bu kodun oluşturulması gerekmektedir. Bu kimlik kodları, sadece bir kez Apple’a aktarılır. Bu süreçte verilerin ABD’ye aktarılması da söz konusu olabilir. Orada Avrupa hukukuna uygun kişisel verilerin koruma seviyesi bulunmamaktadır ve Avrupa’daki veri koruma haklarınız uygulanmayabilir. Bu bağlamda özellikle ABD güvenlik makamlarının, somut bir şüphe olmasa bile, Apple’a aktarılan bu verilere erişme ve örneğin verileri diğer bilgilerle ilişkilendirerek bunları değerlendirmeye alma olasılığı bulunmaktadır. Bu durum sadece Apple’a aktarılan kimlik kodları için söz konusudur. Apple ankete katılımız ile ilgili daha fazla bilgi almaz."; "SurveyConsent_Details_Legal_Body2" = "Üçüncü ülkelere aktarımı kabul etmiyorsanız, lütfen “Kabul ediyorum” seçeneğine tıklamayın. Buna rağmen Uygulamayı kullanmaya devam edebilirsiniz, ancak ankete katılamazsınız."; + +/* Data Donation App Settings*/ + +"ppa_settings_privacy_information_body" = "Yukarıdaki “Veri bağışı” seçeneğini etkinleştirerek, şunlara onay vermiş olursunuz:\n\nUygulama, topladığı bilgileri her gün RKI’ye aktarır. Bunlar, görüntülenen riskli karşılaşmalar ve uyarılar, size gönderilen test sonuçları, diğer kullanıcıları uyarıp uyarmadığınız ve akıllı telefonunuzun işletim sistemine ilişkin verilerdir. Ayrıca başka bilgiler de verdiyseniz (bölge, yaş grubu gibi), bunlar da RKI’ye aktarılır.\n\nRKI, Uygulamanın etki gücünü ve işlevselliğini değerlendirmek ve pandemi hakkında yeni çıkarımlar elde etmek için, bu verileri birleştirecek ve istatistikler olarak değerlendirecektir. Bu süreçte edinilen bulgular, Uygulamanın işlevlerini ve kullanım kolaylığını iyileştirmenin yanı sıra pandemiye karşı mücadele için diğer önlemlerin yönlendirilmesine yardımcı olmaktadır.\n\nVerilerinizin değerlendirilmesinden önce, veri bağışına katılan her Uygulamanın yalnızca bir kez sayıma alındığı ve istatistiklerin tahrif edilmediği kontrol edilir. Bu bağlamda Uygulamanızın orijinal olduğunun incelenmesi gerekmektedir. Bunun için, akıllı telefonunuz tarafından benzersiz bir kimlik kodu oluşturulur ve Apple’ın Uygulamanızın orijinal ürün olduğunu RKI’ye doğrulaması için ABD’deki Apple’a aktarılır. Bu kimlik kodu, akıllı telefonunuzun sürümü ve Uygulama hakkında veriler içerir. Apple, Uygulamadan daha fazla bilgi almaz.\n\nYukarıdaki “Veri bağışı” seçeneğini devre dışı bırakarak, verdiğiniz rıza beyanını geri alabilirsiniz."; diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewController.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewController.swift similarity index 74% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewController.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewController.swift index 69835edc744..81eb4066945 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewController.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewController.swift @@ -1,4 +1,4 @@ -//// +// // 🦠 Corona-Warn-App // @@ -9,26 +9,9 @@ class DataDonationViewController: DynamicTableViewController, DeltaOnboardingVie // MARK: - Init init( - store: Store, - presentSelectValueList: @escaping (SelectValueViewModel) -> Void, - didTapLegal: @escaping () -> Void + viewModel: DataDonationViewModelProtocol ) { - self.presentSelectValueList = presentSelectValueList - self.didTapLegal = didTapLegal - - guard let url = Bundle.main.url(forResource: "ppdd-ppa-administrative-unit-set-ua-approved", withExtension: "json") else { - preconditionFailure("missing json file") - } - let datadonationModel = DataDonationModel( - store: store, - jsonFileURL: url - ) - - self.viewModel = DataDonationViewModel( - store: store, - presentSelectValueList: presentSelectValueList, - datadonationModel: datadonationModel - ) + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -45,24 +28,11 @@ class DataDonationViewController: DynamicTableViewController, DeltaOnboardingVie setupTableView() } + override var navigationItem: UINavigationItem { navigationFooterItem } - - private lazy var navigationFooterItem: ENANavigationFooterItem = { - let item = ENANavigationFooterItem() - - item.primaryButtonTitle = AppStrings.DataDonation.Info.buttonOK - item.isPrimaryButtonEnabled = true - - item.secondaryButtonTitle = AppStrings.DataDonation.Info.buttonNOK - item.secondaryButtonHasBackground = true - item.isSecondaryButtonHidden = false - item.isSecondaryButtonEnabled = true - return item - }() - // MARK: - Protocol ENANavigationControllerWithFooterChild func navigationController(_ navigationController: ENANavigationControllerWithFooter, didTapPrimaryButton button: UIButton) { @@ -78,23 +48,33 @@ class DataDonationViewController: DynamicTableViewController, DeltaOnboardingVie // MARK: - Protocol DismissHandling func wasAttemptedToBeDismissed() { - + Log.debug("attemptedToBeDismissed") } - // MARK: - Public - // MARK: - Internal /// Is called when when the one of the ENANavigationControllerWithFooter buttons is tapped. var finished: (() -> Void)? // MARK: - Private - private let presentSelectValueList: (SelectValueViewModel) -> Void - private let didTapLegal: () -> Void - private let viewModel: DataDonationViewModel + private let viewModel: DataDonationViewModelProtocol private var subscriptions: [AnyCancellable] = [] + private lazy var navigationFooterItem: ENANavigationFooterItem = { + let item = ENANavigationFooterItem() + + item.primaryButtonTitle = AppStrings.DataDonation.Info.buttonOK + item.isPrimaryButtonEnabled = true + + item.secondaryButtonTitle = AppStrings.DataDonation.Info.buttonNOK + item.secondaryButtonHasBackground = true + item.isSecondaryButtonHidden = false + item.isSecondaryButtonEnabled = true + + return item + }() + private func setupTableView() { view.backgroundColor = .enaColor(for: .background) tableView.separatorStyle = .none @@ -111,19 +91,21 @@ class DataDonationViewController: DynamicTableViewController, DeltaOnboardingVie dynamicTableViewModel = viewModel.dynamicTableViewModel - viewModel.$reloadTableView + viewModel.dataDonationModelPublisher .receive(on: DispatchQueue.OCombine(.main)) .sink { [weak self] _ in guard let self = self else { return } self.dynamicTableViewModel = self.viewModel.dynamicTableViewModel - self.tableView.reloadData() + DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.35) { + self.tableView.reloadData() + } }.store(in: &subscriptions) } } // MARK: - Cell reuse identifiers. -extension DataDonationViewController { +internal extension DataDonationViewController { enum CustomCellReuseIdentifiers: String, TableViewCellReuseIdentifiers { case roundedCell case legalExtended = "DynamicLegalExtendedCell" diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/BaseDataDonationViewModel.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/BaseDataDonationViewModel.swift new file mode 100644 index 00000000000..59eea07cd25 --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/BaseDataDonationViewModel.swift @@ -0,0 +1,100 @@ +// +// 🦠 Corona-Warn-App +// + +import Foundation +import UIKit +import OpenCombine + +/*** this protocol gets used to provide different view model classes for the dataDonationViewController */ + +protocol DataDonationViewModelProtocol { + + // dataDonationModel and it's Publisher to subscribe changes + var dataDonationModel: DataDonationModel { get } + var dataDonationModelPublisher: OpenCombine.Published.Publisher { get } + + var friendlyFederalStateName: String { get } + var friendlyRegionName: String { get } + var friendlyAgeName: String { get } + var dynamicTableViewModel: DynamicTableViewModel { get } + + func save(consentGiven: Bool) +} + +class BaseDataDonationViewModel: DataDonationViewModelProtocol { + + // MARK: - Init + + init( + store: Store, + presentSelectValueList: @escaping (SelectValueViewModel) -> Void, + datadonationModel: DataDonationModel + ) { + self.presentSelectValueList = presentSelectValueList + self.dataDonationModel = datadonationModel + } + + // MARK: - Protocol DataDonationViewModelProtocol + + let presentSelectValueList: (SelectValueViewModel) -> Void + + @OpenCombine.Published var dataDonationModel: DataDonationModel + var dataDonationModelPublisher: OpenCombine.Published.Publisher { $dataDonationModel } + + var subscriptions: [AnyCancellable] = [] + + var dynamicTableViewModel: DynamicTableViewModel { + fatalError("base implementation should never get called") + } + + /// formatted name output + var friendlyFederalStateName: String { + return dataDonationModel.federalStateName ?? AppStrings.DataDonation.Info.noSelectionState + } + + /// formatted region output + var friendlyRegionName: String { + return dataDonationModel.region ?? AppStrings.DataDonation.Info.noSelectionRegion + } + + /// formatted age output + var friendlyAgeName: String { + return dataDonationModel.age ?? AppStrings.DataDonation.Info.noSelectionAgeGroup + } + + /// will set consent given and save the model afterwards + func save(consentGiven: Bool) { + dataDonationModel.isConsentGiven = consentGiven + Log.debug("DataDonation consent value set to '\(consentGiven)'") + dataDonationModel.save() + } +} + +internal extension DynamicCell { + + /// A `legalExtendedDataDonation` to display legal text for Data Donation screen + /// - Parameters: + /// - title: The title/header for the legal foo. + /// - description: Optional description text. + /// - bulletPoints: A list of strings to be prefixed with bullet points. + /// - accessibilityIdentifier: Optional, but highly recommended, accessibility identifier. + /// - configure: Optional custom cell configuration + /// - Returns: A `DynamicCell` to display legal texts + static func legalExtendedDataDonation( + title: NSAttributedString, + description: NSAttributedString?, + bulletPoints: [NSAttributedString]? = nil, + accessibilityIdentifier: String? = nil, + configure: CellConfigurator? = nil + ) -> Self { + .identifier(DataDonationViewController.CustomCellReuseIdentifiers.legalExtended) { viewController, cell, indexPath in + guard let cell = cell as? DynamicLegalExtendedCell else { + fatalError("could not initialize cell of type `DynamicLegalExtendedCell`") + } + cell.configure(title: title, description: description, bulletPoints: bulletPoints) + configure?(viewController, cell, indexPath) + } + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewModel.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/DefaultDataDonationViewModel.swift similarity index 66% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewModel.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/DefaultDataDonationViewModel.swift index 4a2b618c151..4eb8da3d3dd 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationViewModel.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/DefaultDataDonationViewModel.swift @@ -1,4 +1,4 @@ -//// +// // 🦠 Corona-Warn-App // @@ -6,45 +6,12 @@ import Foundation import UIKit import OpenCombine -final class DataDonationViewModel { - - // MARK: - Init - - init( - store: Store, - presentSelectValueList: @escaping (SelectValueViewModel) -> Void, - datadonationModel: DataDonationModel - ) { - self.presentSelectValueList = presentSelectValueList - self.reloadTableView = false - self.dataDonationModel = datadonationModel - } - - // MARK: - Public - - func save(consentGiven: Bool) { - dataDonationModel.isConsentGiven = consentGiven - Log.debug("DataDonation consent value set to '\(consentGiven)'") - dataDonationModel.save() - } - - // MARK: - Internal - - @OpenCombine.Published private (set) var reloadTableView: Bool - - var friendlyFederalStateName: String { - return dataDonationModel.federalStateName ?? AppStrings.DataDonation.Info.noSelectionState - } - - var friendlyRegionName: String { - return dataDonationModel.region ?? AppStrings.DataDonation.Info.noSelectionRegion - } +final class DefaultDataDonationViewModel: BaseDataDonationViewModel { - var friendlyAgeName: String { - return dataDonationModel.age ?? AppStrings.DataDonation.Info.noSelectionAgeGroup - } + // MARK: - Overrides - var dynamicTableViewModel: DynamicTableViewModel { + /// override layout of the dynamiv tableview model + override var dynamicTableViewModel: DynamicTableViewModel { /// create the top section with the illustration and title text var dynamicTableViewModel = DynamicTableViewModel.with { $0.add( @@ -136,14 +103,9 @@ final class DataDonationViewModel { return dynamicTableViewModel } - // MARK: - Private - - private let presentSelectValueList: (SelectValueViewModel) -> Void - - private var dataDonationModel: DataDonationModel - private var subscriptions: [AnyCancellable] = [] + // MARK: - Internal - private func didTapSelectStateButton() { + func didTapSelectStateButton() { let selectValueViewModel = SelectValueViewModel( dataDonationModel.allFederalStateNames, title: AppStrings.DataDonation.ValueSelection.Title.State, @@ -156,12 +118,11 @@ final class DataDonationViewModel { // if a new fedaral state got selected reset region as well self?.dataDonationModel.federalStateName = federalState self?.dataDonationModel.region = nil - self?.reloadTableView.toggle() }.store(in: &subscriptions) presentSelectValueList(selectValueViewModel) } - private func didTapSelectRegionButton() { + func didTapSelectRegionButton() { guard let federalStateName = dataDonationModel.federalStateName else { Log.debug("Missing federal state to load regions", log: .ppac) return @@ -177,13 +138,12 @@ final class DataDonationViewModel { return } self?.dataDonationModel.region = region - self?.reloadTableView.toggle() }.store(in: &subscriptions) presentSelectValueList(selectValueViewModel) } - private func didTapAgeButton() { + func didTapAgeButton() { let selectValueViewModel = SelectValueViewModel( AgeGroup.allCases.map({ $0.text }), title: AppStrings.DataDonation.ValueSelection.Title.Age, @@ -194,38 +154,9 @@ final class DataDonationViewModel { return } self?.dataDonationModel.age = age - self?.reloadTableView.toggle() }.store(in: &subscriptions) presentSelectValueList(selectValueViewModel) } } - -extension DynamicCell { - - /// A `legalExtendedDataDonation` to display legal text for Data Donation screen - /// - Parameters: - /// - title: The title/header for the legal foo. - /// - description: Optional description text. - /// - bulletPoints: A list of strings to be prefixed with bullet points. - /// - accessibilityIdentifier: Optional, but highly recommended, accessibility identifier. - /// - configure: Optional custom cell configuration - /// - Returns: A `DynamicCell` to display legal texts - static func legalExtendedDataDonation( - title: NSAttributedString, - description: NSAttributedString?, - bulletPoints: [NSAttributedString]? = nil, - accessibilityIdentifier: String? = nil, - configure: CellConfigurator? = nil - ) -> Self { - .identifier(DataDonationViewController.CustomCellReuseIdentifiers.legalExtended) { viewController, cell, indexPath in - guard let cell = cell as? DynamicLegalExtendedCell else { - fatalError("could not initialize cell of type `DynamicLegalExtendedCell`") - } - cell.configure(title: title, description: description, bulletPoints: bulletPoints) - configure?(viewController, cell, indexPath) - } - } - -} diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/SettingsDataDonationViewModel.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/SettingsDataDonationViewModel.swift new file mode 100644 index 00000000000..99591741c93 --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/SettingsDataDonationViewModel.swift @@ -0,0 +1,222 @@ +// +// 🦠 Corona-Warn-App +// + +import Foundation +import UIKit +import OpenCombine + +final class SettingsDataDonationViewModel: BaseDataDonationViewModel { + + // MARK: - Overrides + + /// we use a slitly different string if no federal state was selected + override var friendlyFederalStateName: String { + return dataDonationModel.federalStateName ?? AppStrings.DataDonation.Info.subHeadState + } + + /// we use a slitly different string if no age was given + override var friendlyAgeName: String { + return dataDonationModel.age ?? AppStrings.DataDonation.Info.subHeadAgeGroup + } + + override var dynamicTableViewModel: DynamicTableViewModel { + /// create the top section with the illustration and title text + var dynamicTableViewModel = DynamicTableViewModel.with { + $0.add( + .section( + header: .image( + UIImage(named: "Illu_DataDonation"), + accessibilityLabel: "AppStrings.DataDonation.Info.accImageDescription", + accessibilityIdentifier: "AccessibilityIdentifiers.DataDonation.accImageDescription", + height: 250 + ), + cells: [ + .title1(text: AppStrings.DataDonation.Info.title, accessibilityIdentifier: "AppStrings.DataDonation.Info.title"), + .headline(text: AppStrings.DataDonation.Info.description) + ] + ) + ) + } + + /// section to show input fields with already given data + /// this will change numer of cells by the already entered data + let sectionCells: [DynamicCell] = [ + .footnote(text: AppStrings.DataDonation.Info.settingsSubHeadline, accessibilityIdentifier: nil), + .body( + text: AppStrings.Settings.Datadonation.label, + style: .label, + color: nil, + accessibilityIdentifier: nil, + accessibilityTraits: .staticText, + action: .none, + configure: { [weak self] _, cell, _ in + guard let self = self else { + return + } + let toggleSwitch = UISwitch() + cell.accessoryView = toggleSwitch + toggleSwitch.isOn = self.dataDonationModel.isConsentGiven + toggleSwitch.onTintColor = .enaColor(for: .tint) + toggleSwitch.addTarget(self, action: #selector(self.didToggleDatadonationSwitch), for: .valueChanged) + }), + + dataDonationModel.isConsentGiven == true ? + .body( + text: friendlyFederalStateName, + style: .label, + accessibilityTraits: .button, + action: .execute(block: { [weak self] _, _ in + self?.didTapSelectStateButton() + }), + configure: { _, cell, _ in + cell.accessoryType = .disclosureIndicator + }): + nil, + + dataDonationModel.isConsentGiven == true ? + .body( + text: friendlyRegionName, + style: .label, accessibilityIdentifier: nil, + accessibilityTraits: .button, + action: .execute(block: { [weak self] _, _ in + self?.didTapSelectRegionButton() + }), + configure: { _, cell, _ in + cell.accessoryType = .disclosureIndicator + }) : + nil, + + dataDonationModel.isConsentGiven == true ? + .body( + text: friendlyAgeName, + style: .label, + color: nil, + accessibilityIdentifier: nil, + accessibilityTraits: .button, + action: .execute(block: { [weak self] _, _ in + self?.didTapAgeButton() + }), + configure: { _, cell, _ in + cell.accessoryType = .disclosureIndicator + }) : + nil + ] + .compactMap { $0 } + + dynamicTableViewModel.add( + .section( + cells: sectionCells + ) + ) + + /// section for the legal text + dynamicTableViewModel.add( + .section( + cells: [ + .legalExtendedDataDonation( + title: NSAttributedString(string: AppStrings.DataDonation.Info.legalTitle), + description: NSAttributedString( + string: AppStrings.DataDonation.AppSettings.ppaSettingsPrivacyInformationBody, + attributes: [.font: UIFont.preferredFont(forTextStyle: .body)]), + bulletPoints: [ + NSAttributedString(string: AppStrings.DataDonation.Info.legalAcknowledgementBulletPoint1), + NSAttributedString(string: AppStrings.DataDonation.Info.legalAcknowledgementBulletPoint2), + NSAttributedString(string: AppStrings.DataDonation.Info.legalAcknowledgementBulletPoint3)], + accessibilityIdentifier: AppStrings.DataDonation.Info.legalTitle + ) + ] + ) + ) + + dynamicTableViewModel.add( + .section(separators: .all, cells: [ + .body( + text: AppStrings.DataDonation.Info.dataProcessingDetails, + style: DynamicCell.TextCellStyle.label, + accessibilityIdentifier: AccessibilityIdentifiers.ExposureSubmissionQRInfo.dataProcessingDetailInfo, + accessibilityTraits: UIAccessibilityTraits.link, + action: .pushDataDonationDetails(model: DataDonationDetailsViewModel().dynamicTableViewModel, + withTitle: AppStrings.DataDonation.DetailedInfo.title, + completion: nil + ), + configure: { _, cell, _ in + cell.accessoryType = .disclosureIndicator + cell.selectionStyle = .default + }), + .space(height: 12) + ]) + ) + + return dynamicTableViewModel + } + + // MARK: - Internal + + @objc /// consent given switch changes + func didToggleDatadonationSwitch(sender: UISwitch) { + save(consentGiven: sender.isOn) + } + + /// user tapped select state + func didTapSelectStateButton() { + let selectValueViewModel = SelectValueViewModel( + dataDonationModel.allFederalStateNames, + title: AppStrings.DataDonation.ValueSelection.Title.State, + preselected: dataDonationModel.federalStateName + ) + selectValueViewModel.$selectedValue.sink { [weak self] federalState in + guard self?.dataDonationModel.federalStateName != federalState else { + return + } + // if a new fedaral state got selected reset region as well + self?.dataDonationModel.federalStateName = federalState + self?.dataDonationModel.region = nil + self?.dataDonationModel.save() + }.store(in: &subscriptions) + presentSelectValueList(selectValueViewModel) + } + + /// user tapped select region + func didTapSelectRegionButton() { + guard let federalStateName = dataDonationModel.federalStateName else { + Log.debug("Missing federal state to load regions", log: .ppac) + return + } + + let selectValueViewModel = SelectValueViewModel( + dataDonationModel.allRegions(by: federalStateName), + title: AppStrings.DataDonation.ValueSelection.Title.Region, + preselected: dataDonationModel.region + ) + selectValueViewModel.$selectedValue .sink { [weak self] region in + guard self?.dataDonationModel.region != region else { + return + } + self?.dataDonationModel.region = region + self?.dataDonationModel.save() + }.store(in: &subscriptions) + + presentSelectValueList(selectValueViewModel) + } + + /// user tapped select age + func didTapAgeButton() { + let selectValueViewModel = SelectValueViewModel( + AgeGroup.allCases.map({ $0.text }), + title: AppStrings.DataDonation.ValueSelection.Title.Age, + preselected: dataDonationModel.age + ) + selectValueViewModel.$selectedValue .sink { [weak self] age in + guard self?.dataDonationModel.age != age else { + return + } + self?.dataDonationModel.age = age + self?.dataDonationModel.save() +// self?.reloadTableView.toggle() + }.store(in: &subscriptions) + + presentSelectValueList(selectValueViewModel) + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationViewModelTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/BaseDataDonationViewModelTests.swift similarity index 88% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationViewModelTests.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/BaseDataDonationViewModelTests.swift index 26c398e652b..304958fdc5b 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationViewModelTests.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/BaseDataDonationViewModelTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import ENA -class DataDonationViewModelTests: XCTestCase { +class BaseDataDonationViewModelTests: XCTestCase { /// test if the view model will format texts correct func testGIVEN_ViewModelWithStoredData_WHEN_getFriendlyTexts_THEN_ValuesAreEqualToStore() throws { @@ -17,7 +17,7 @@ class DataDonationViewModelTests: XCTestCase { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) - let viewModel = DataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + let viewModel = BaseDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) // WHEN let friendlyFederalStateName = viewModel.friendlyFederalStateName @@ -38,7 +38,7 @@ class DataDonationViewModelTests: XCTestCase { let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) - let viewModel = DataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + let viewModel = BaseDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) // WHEN let friendlyFederalStateName = viewModel.friendlyFederalStateName @@ -63,7 +63,7 @@ class DataDonationViewModelTests: XCTestCase { model.federalStateName = "Hessen" model.age = AgeGroup.ageBelow29.text - let viewModel = DataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + let viewModel = BaseDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) // WHEN viewModel.save(consentGiven: true) @@ -88,7 +88,7 @@ class DataDonationViewModelTests: XCTestCase { model.federalStateName = "Hessen" model.age = AgeGroup.ageBelow29.text - let viewModel = DataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + let viewModel = BaseDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) // WHEN viewModel.save(consentGiven: true) @@ -113,7 +113,7 @@ class DataDonationViewModelTests: XCTestCase { model.federalStateName = "Hessen" model.age = AgeGroup.ageBelow29.text - let viewModel = DataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + let viewModel = BaseDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) // WHEN viewModel.save(consentGiven: false) diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/DefaultDataDonationViewModelTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/DefaultDataDonationViewModelTests.swift new file mode 100644 index 00000000000..f2b751afd0f --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/DefaultDataDonationViewModelTests.swift @@ -0,0 +1,92 @@ +//// +// 🦠 Corona-Warn-App +// + +import XCTest +@testable import ENA + +class DefaultDataDonationViewModelTests: XCTestCase { + + func testGIVEN_DataDonationModel_WHEN_getDynamicTableViewModel_THEN_SectionsAndCellCountsMatch() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + + let viewModel = DefaultDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + + // WHEN + let dynamicTableViewModel = viewModel.dynamicTableViewModel + + // THEN + XCTAssertEqual(dynamicTableViewModel.numberOfSection, 4) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 0), 2) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 1), 5) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 2), 1) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 3), 2) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectState_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = DefaultDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapSelectStateButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectRegion_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = DefaultDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapSelectRegionButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectAge_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = DefaultDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapAgeButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/SettingsDataDonationViewModelTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/SettingsDataDonationViewModelTests.swift new file mode 100644 index 00000000000..e6df11b97b1 --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/SettingsDataDonationViewModelTests.swift @@ -0,0 +1,114 @@ +//// +// 🦠 Corona-Warn-App +// + +import XCTest +@testable import ENA + +class SettingsDataDonationViewModelTests: XCTestCase { + + /// test if the view model will format empty texts correct + func testGIVEN_ViewModelWithoutStoredData_WHEN_getFriendlyTexts_THEN_ValuesAreEqualToStore() throws { + // GIVEN + let mockStore = MockTestStore() + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + + let viewModel = SettingsDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + + // WHEN + let friendlyFederalStateName = viewModel.friendlyFederalStateName + let friendlyRegionName = viewModel.friendlyRegionName + let friendlyAgeName = viewModel.friendlyAgeName + + // THEN + + XCTAssertEqual(friendlyFederalStateName, AppStrings.DataDonation.Info.subHeadState ) + XCTAssertEqual(friendlyRegionName, AppStrings.DataDonation.Info.noSelectionRegion) + XCTAssertEqual(friendlyAgeName, AppStrings.DataDonation.Info.subHeadAgeGroup) + } + + func testGIVEN_DataDonationModel_WHEN_getDynamicTableViewModel_THEN_SectionsAndCellCountsMatch() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = false + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + + let viewModel = SettingsDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in }, datadonationModel: model) + + // WHEN + let dynamicTableViewModel = viewModel.dynamicTableViewModel + + // THEN + XCTAssertEqual(dynamicTableViewModel.numberOfSection, 4) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 0), 2) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 1), 2) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 2), 1) + XCTAssertEqual(dynamicTableViewModel.numberOfRows(section: 3), 2) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectState_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = SettingsDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapSelectStateButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectRegion_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = SettingsDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapSelectRegionButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + + func testGIVEN_DataDonationModel_WHEN_TapSelectAge_THEN_ClosureGetsCalled() throws { + // GIVEN + let mockStore = MockTestStore() + mockStore.isPrivacyPreservingAnalyticsConsentGiven = true + mockStore.userMetadata = UserMetadata(federalState: FederalStateName.schleswigHolstein, administrativeUnit: 11001053, ageGroup: .ageBelow29) + + let fileURL = try XCTUnwrap(Bundle(for: type(of: self)).url(forResource: "testData", withExtension: "json")) + let model = DataDonationModel(store: mockStore, jsonFileURL: fileURL) + let expectationPresentList = expectation(description: "Present value list hit") + let viewModel = SettingsDataDonationViewModel(store: mockStore, presentSelectValueList: { _ in + expectationPresentList.fulfill() + }, datadonationModel: model) + + // WHEN + viewModel.didTapAgeButton() + + // THEN + wait(for: [expectationPresentList], timeout: .medium) + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/testData.json b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/testData.json similarity index 100% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/testData.json rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/testData.json diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/testDataInvalid.json b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/testDataInvalid.json similarity index 100% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/testDataInvalid.json rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/DataDonationViewModels/__tests__/testDataInvalid.json diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationModel.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/DataDonationModel.swift similarity index 94% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationModel.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/DataDonationModel.swift index b86a15e13f3..99eb0669689 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationModel.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/DataDonationModel.swift @@ -1,4 +1,4 @@ -//// +// // 🦠 Corona-Warn-App // @@ -32,8 +32,6 @@ struct DataDonationModel { }?.districtName } - // MARK: - Public - // MARK: - Internal var isConsentGiven: Bool @@ -57,7 +55,6 @@ struct DataDonationModel { mutating func save() { store.isPrivacyPreservingAnalyticsConsentGiven = isConsentGiven guard isConsentGiven else { - store.userMetadata = UserMetadata(federalState: nil, administrativeUnit: nil, ageGroup: nil) region = nil federalStateName = nil age = nil diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/District.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/District.swift similarity index 97% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/District.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/District.swift index 995854c2a19..e95aaee84c3 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/District.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/District.swift @@ -78,7 +78,7 @@ enum FederalStateName: String, CaseIterable, Codable { } } -enum FederalStateShortName: String, Codable { +enum FederalStateShortName: String, CaseIterable, Codable { case bb = "BB" case be = "BE" case bw = "BW" diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationModelTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DataDonationModelTests.swift similarity index 99% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationModelTests.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DataDonationModelTests.swift index c489f6acaff..f4d0047124c 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/__tests__/DataDonationModelTests.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DataDonationModelTests.swift @@ -1,4 +1,4 @@ -//// +// // 🦠 Corona-Warn-App // diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DistrictTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DistrictTests.swift new file mode 100644 index 00000000000..89566619b91 --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonation/Model/__tests__/DistrictTests.swift @@ -0,0 +1,40 @@ +// +// 🦠 Corona-Warn-App +// + +import XCTest +@testable import ENA + +class DistrictTests: XCTestCase { + + func testGIVEN_FederalStateName_THEN_MatchesLastKnownValues() { + // GIVEN + let countStates = FederalStateName.allCases.count + let countShortname = FederalStateShortName.allCases.count + + // THEN + XCTAssertEqual(countStates, 16) + XCTAssertEqual(countShortname, 16) + } + + func testGIVEN_givenDistrictElement_THEN_ValuesAreSet() { + // GIVEN + let districtElement = DistrictElement( + districtName: "Göppingen", + districtShortName: "BW", + districtID: 1, + federalStateName: .badenWürttemberg, + federalStateShortName: .bw, + federalStateID: 1 + ) + + // THEN + XCTAssertEqual(districtElement.districtName, "Göppingen") + XCTAssertEqual(districtElement.districtShortName, "BW") + XCTAssertEqual(districtElement.districtID, 1) + XCTAssertEqual(districtElement.federalStateName, .badenWürttemberg) + XCTAssertEqual(districtElement.federalStateShortName, .bw) + XCTAssertEqual(districtElement.federalStateID, 1) + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationDetailsViewController.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/DataDonationDetailsViewController.swift similarity index 100% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationDetailsViewController.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/DataDonationDetailsViewController.swift diff --git a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationDetailsViewModel.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/DataDonationDetailsViewModel.swift similarity index 98% rename from src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationDetailsViewModel.swift rename to src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/DataDonationDetailsViewModel.swift index 01a4c436cd5..9a5feff1100 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Datadonation/DataDonationDetailsViewModel.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/DataDonationDetailsViewModel.swift @@ -39,11 +39,10 @@ final class DataDonationDetailsViewModel { ) } ), - .space(height: 8), + .space(height: 20), .headline(text: AppStrings.DataDonation.DetailedInfo.headline, accessibilityIdentifier: "AppStrings.DataDonation.DetailedInfo.headline"), - .space(height: 8), + .space(height: 20), .body(text: AppStrings.DataDonation.DetailedInfo.paragraph1, accessibilityIdentifier: "AppStrings.DataDonation.DetailedInfo.paragraph1"), - .space(height: 8), bulletPointCellWithBoldHeadline[0], bulletPointCellWithBoldHeadline[1], diff --git a/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/__tests__/DataDonationDetailsViewModelTests.swift b/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/__tests__/DataDonationDetailsViewModelTests.swift new file mode 100644 index 00000000000..9515f39ff1d --- /dev/null +++ b/src/xcode/ENA/ENA/Source/Scenes/DataDonationDetails/__tests__/DataDonationDetailsViewModelTests.swift @@ -0,0 +1,76 @@ +//// +// 🦠 Corona-Warn-App +// + +import XCTest +@testable import ENA + +class DataDonationDetailsViewModelTests: XCTestCase { + + let totalNumberOfCells = 35 + let indexOfBulletPointCells = [7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 27, 28, 29, 31, 32, 33] + let indexOfSpaceCells = [1, 3, 5] + let indexOfLabelCells = [0, 4, 6, 11, 16, 22, 30, 34] + let model = DataDonationDetailsViewModel() + + func testForOneSection() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + let numberOfSections = tableViewModel.content.count + // THEN + XCTAssertEqual(numberOfSections, 1) + } + + func testForNumberOfCells() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + let numberOfCells = tableViewModel.section(0).cells.count + // THEN + XCTAssertEqual(numberOfCells, totalNumberOfCells) + } + + func testReuseIdentifierRoundedCell() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + let cell2 = tableViewModel.section(0).cells[2] + // THEN + XCTAssertEqual(cell2.cellReuseIdentifier.rawValue, "roundedCell") + } + + func testReuseIdentifierBulletPointCell() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + for i in indexOfBulletPointCells { + let cell = tableViewModel.section(0).cells[i] + // THEN + XCTAssertEqual(cell.cellReuseIdentifier.rawValue, "bulletPointCell") + } + } + + func testReuseIdentifierLabelCell() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + for i in indexOfLabelCells { + let cell = tableViewModel.section(0).cells[i] + // THEN + XCTAssertEqual(cell.cellReuseIdentifier.rawValue, "labelCell") + } + } + + func testReuseIdentifierSpaceCell() throws { + // GIVEN + let tableViewModel = model.dynamicTableViewModel + // WHEN + for i in indexOfSpaceCells { + let cell = tableViewModel.section(0).cells[i] + // THEN + XCTAssertEqual(cell.cellReuseIdentifier.rawValue, "spaceCell") + } + } + +} diff --git a/src/xcode/ENA/ENA/Source/Scenes/Onboarding/DeltaOnboarding/NewVersionFeatures/DeltaOnboardingDataDonation.swift b/src/xcode/ENA/ENA/Source/Scenes/Onboarding/DeltaOnboarding/NewVersionFeatures/DeltaOnboardingDataDonation.swift index 654f8fcf363..1499a855f5f 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Onboarding/DeltaOnboarding/NewVersionFeatures/DeltaOnboardingDataDonation.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/Onboarding/DeltaOnboarding/NewVersionFeatures/DeltaOnboardingDataDonation.swift @@ -14,10 +14,13 @@ class DeltaOnboardingDataDonation: DeltaOnboarding { } func makeViewController() -> DeltaOnboardingViewControllerProtocol { - + guard let url = Bundle.main.url(forResource: "ppdd-ppa-administrative-unit-set-ua-approved", withExtension: "json") else { + preconditionFailure("missing json file") + } + weak var navigationController: DeltaOnboardingNavigationController? - - let dataDonationViewController = DataDonationViewController( + + let viewModel = DefaultDataDonationViewModel( store: store, presentSelectValueList: { selectValueViewModel in let selectValueViewController = SelectValueTableViewController( @@ -28,9 +31,13 @@ class DeltaOnboardingDataDonation: DeltaOnboarding { let selectValueNavigationController = UINavigationController(rootViewController: selectValueViewController) navigationController?.present(selectValueNavigationController, animated: true) }, - didTapLegal: {} + datadonationModel: DataDonationModel( + store: store, + jsonFileURL: url + ) ) + let dataDonationViewController = DataDonationViewController(viewModel: viewModel) let deltaOnboardingNavigationController = DeltaOnboardingNavigationController(rootViewController: dataDonationViewController) deltaOnboardingNavigationController.navigationBar.prefersLargeTitles = true diff --git a/src/xcode/ENA/ENA/Source/Scenes/Onboarding/OnboardingInfoViewController.swift b/src/xcode/ENA/ENA/Source/Scenes/Onboarding/OnboardingInfoViewController.swift index 1f05e260703..3eaf81636f6 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Onboarding/OnboardingInfoViewController.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/Onboarding/OnboardingInfoViewController.swift @@ -147,9 +147,14 @@ final class OnboardingInfoViewController: UIViewController { } private func gotoDataDonationScreen() { - let dataDonationViewController = DataDonationViewController( + + guard let jsonFileURL = Bundle.main.url(forResource: "ppdd-ppa-administrative-unit-set-ua-approved", withExtension: "json") else { + preconditionFailure("missing json file") + } + + let viewModel = DefaultDataDonationViewModel( store: store, - presentSelectValueList: { [weak self] selectValueViewModel in + presentSelectValueList: { [weak self] selectValueViewModel in let selectValueViewController = SelectValueTableViewController( selectValueViewModel, dissmiss: { [weak self] in @@ -158,9 +163,14 @@ final class OnboardingInfoViewController: UIViewController { let selectValueNavigationController = UINavigationController(rootViewController: selectValueViewController) self?.navigationController?.present(selectValueNavigationController, animated: true) }, - didTapLegal: {} + datadonationModel: DataDonationModel( + store: store, + jsonFileURL: jsonFileURL + ) ) - + + let dataDonationViewController = DataDonationViewController(viewModel: viewModel) + dataDonationViewController.finished = { [weak self] in self?.finishOnBoarding() } diff --git a/src/xcode/ENA/ENA/Source/Scenes/SelectValueViewController/SelectValueTableViewController.swift b/src/xcode/ENA/ENA/Source/Scenes/SelectValueViewController/SelectValueTableViewController.swift index 8628de80caa..ec2aec04a09 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/SelectValueViewController/SelectValueTableViewController.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/SelectValueViewController/SelectValueTableViewController.swift @@ -11,10 +11,12 @@ final class SelectValueTableViewController: UITableViewController { init( _ viewModel: SelectValueViewModel, - dissmiss: @escaping () -> Void + dissmiss: @escaping () -> Void, + closeOnSelection: Bool = true ) { self.viewModel = viewModel self.dissmiss = dissmiss + self.closeOnSelection = closeOnSelection self.subscriptions = [] /// we use .grouped to make seperators end with the last value super.init(style: .grouped) @@ -50,12 +52,16 @@ final class SelectValueTableViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { viewModel.selectValue(at: indexPath) + if closeOnSelection { + DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.38, execute: dissmiss) + } } // MARK: - Private private let viewModel: SelectValueViewModel private let dissmiss: () -> Void + private let closeOnSelection: Bool private var subscriptions: [AnyCancellable] private func setupTableView() { diff --git a/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewController.swift b/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewController.swift index 04ea34aa81f..8937ca1814f 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewController.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewController.swift @@ -17,7 +17,8 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating onTracingCellTap: @escaping () -> Void, onNotificationsCellTap: @escaping () -> Void, onBackgroundAppRefreshCellTap: @escaping () -> Void, - onResetCellTap: @escaping () -> Void + onResetCellTap: @escaping () -> Void, + onDataDonationCellTap: @escaping () -> Void ) { self.store = store self.enState = initialEnState @@ -27,6 +28,7 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating self.onNotificationsCellTap = onNotificationsCellTap self.onBackgroundAppRefreshCellTap = onBackgroundAppRefreshCellTap self.onResetCellTap = onResetCellTap + self.onDataDonationCellTap = onDataDonationCellTap super.init(style: .grouped) } @@ -85,6 +87,8 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating return AppStrings.Settings.resetDescription case .backgroundAppRefresh: return AppStrings.Settings.backgroundAppRefreshDescription + case .datadonation: + return AppStrings.Settings.Datadonation.description } } @@ -96,7 +100,7 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating switch section { case .reset: footerView.textLabel?.textAlignment = .center - case .tracing, .notifications, .backgroundAppRefresh: + case .tracing, .notifications, .backgroundAppRefresh, .datadonation: footerView.textLabel?.textAlignment = .left } } @@ -113,6 +117,8 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating cell = configureMainCell(indexPath: indexPath, model: settingsViewModel.notifications) case .backgroundAppRefresh: cell = configureMainCell(indexPath: indexPath, model: settingsViewModel.backgroundAppRefresh) + case .datadonation: + cell = configureMainCell(indexPath: indexPath, model: settingsViewModel.datadonation) case .reset: guard let labelCell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier.reset.rawValue, for: indexPath) as? SettingsLabelCell else { fatalError("No cell for reuse identifier.") @@ -140,6 +146,8 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating onNotificationsCellTap() case .reset: onResetCellTap() + case .datadonation: + onDataDonationCellTap() case .backgroundAppRefresh: onBackgroundAppRefreshCellTap() } @@ -168,6 +176,7 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating case tracing case notifications case backgroundAppRefresh + case datadonation case reset } @@ -188,12 +197,14 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating private let onNotificationsCellTap: () -> Void private let onBackgroundAppRefreshCellTap: () -> Void private let onResetCellTap: () -> Void + private let onDataDonationCellTap: () -> Void @objc private func updateUI() { checkTracingStatus() checkNotificationSettings() checkBackgroundAppRefresh() + checkDataDonationIsConsentGiven() } private func registerCells() { @@ -263,6 +274,10 @@ final class SettingsViewController: UITableViewController, ExposureStateUpdating ) } + private func checkDataDonationIsConsentGiven() { + settingsViewModel.datadonation.setState(state: store.isPrivacyPreservingAnalyticsConsentGiven) + } + private func configureMainCell(indexPath: IndexPath, model: SettingsViewModel.CellModel) -> MainSettingsCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifier.main.rawValue, for: indexPath) as? MainSettingsCell else { fatalError("No cell for reuse identifier.") diff --git a/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewModel.swift b/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewModel.swift index 2d1e8ada3a0..2dd96eb8cd9 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewModel.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/Settings/Settings/SettingsViewModel.swift @@ -10,6 +10,7 @@ final class SettingsViewModel { var notifications: CellModel = .notifications var backgroundAppRefresh: CellModel = .backgroundAppRefresh var reset: String = AppStrings.Settings.resetLabel + var datadonation: CellModel = .dataDonation } extension SettingsViewModel { @@ -48,6 +49,14 @@ extension SettingsViewModel.CellModel { accessibilityIdentifier: AccessibilityIdentifiers.Settings.backgroundAppRefreshLabel ) + static let dataDonation = SettingsViewModel.CellModel( + icon: "Icons_Settings_Datenspende", + description: AppStrings.Settings.Datadonation.label, + stateActive: AppStrings.Settings.Datadonation.statusActive, + stateInactive: AppStrings.Settings.Datadonation.statusInactive, + accessibilityIdentifier: AccessibilityIdentifiers.Settings.dataDonation + ) + mutating func setState(state newState: Bool) { state = newState ? stateActive : stateInactive } diff --git a/src/xcode/ENA/ENA/Source/Scenes/Settings/SettingsCoordinator.swift b/src/xcode/ENA/ENA/Source/Scenes/Settings/SettingsCoordinator.swift index 864b6025ce0..f032f63fa79 100644 --- a/src/xcode/ENA/ENA/Source/Scenes/Settings/SettingsCoordinator.swift +++ b/src/xcode/ENA/ENA/Source/Scenes/Settings/SettingsCoordinator.swift @@ -67,7 +67,11 @@ class SettingsCoordinator: ENStateHandlerUpdating { }, onResetCellTap: { [weak self] in self?.showResetScreen() + }, + onDataDonationCellTap: { [weak self] in + self?.showDataDonationScreen() } + ) }() @@ -111,5 +115,35 @@ class SettingsCoordinator: ENStateHandlerUpdating { parentNavigationController?.pushViewController(viewController, animated: true) } - + + private func showDataDonationScreen() { + guard let jsonFileURL = Bundle.main.url(forResource: "ppdd-ppa-administrative-unit-set-ua-approved", withExtension: "json") else { + preconditionFailure("missing json file") + } + + let viewModel = SettingsDataDonationViewModel( + store: store, + presentSelectValueList: { [weak self] selectValueViewModel in + self?.presentSelectValueList(selectValueViewModel: selectValueViewModel) + }, + datadonationModel: DataDonationModel( + store: store, + jsonFileURL: jsonFileURL + ) + ) + + let dataDonationViewController = DataDonationViewController(viewModel: viewModel) + parentNavigationController?.pushViewController(dataDonationViewController, animated: true) + } + + private func presentSelectValueList(selectValueViewModel: SelectValueViewModel) { + let selectValueViewController = SelectValueTableViewController( + selectValueViewModel, + dissmiss: { [weak self] in + self?.parentNavigationController?.dismiss(animated: true) + }) + let navigationController = UINavigationController(rootViewController: selectValueViewController) + parentNavigationController?.present(navigationController, animated: true) + } + } diff --git a/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift b/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift index a4fb2fe5a14..dbab82ea462 100644 --- a/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift +++ b/src/xcode/ENA/ENA/Source/Services/__tests__/Mocks/MockTestStore.swift @@ -78,7 +78,16 @@ final class MockTestStore: Store, AppConfigCaching, PrivacyPreservingProviding { // MARK: - PrivacyPreservingProviding - var isPrivacyPreservingAnalyticsConsentGiven: Bool = false + var isPrivacyPreservingAnalyticsConsentGiven: Bool = false { + didSet { + currentRiskExposureMetadata = nil + previousRiskExposureMetadata = nil + userMetadata = nil + lastSubmittedPPAData = nil + lastAppReset = nil + lastSubmissionAnalytics = nil + } + } var otpToken: OTPToken? var otpAuthorizationDate: Date? var ppacApiToken: TimestampedToken? diff --git a/src/xcode/ENA/ENA/Source/View Helpers/AccessibilityIdentifiers.swift b/src/xcode/ENA/ENA/Source/View Helpers/AccessibilityIdentifiers.swift index abf3e4156d8..3839ea60778 100644 --- a/src/xcode/ENA/ENA/Source/View Helpers/AccessibilityIdentifiers.swift +++ b/src/xcode/ENA/ENA/Source/View Helpers/AccessibilityIdentifiers.swift @@ -104,6 +104,7 @@ enum AccessibilityIdentifiers { static let backgroundAppRefreshLabel = "AppStrings.Settings.backgroundAppRefreshLabel" static let resetLabel = "AppStrings.Settings.resetLabel" static let backgroundAppRefreshImageDescription = "AppStrings.Settings.backgroundAppRefreshImageDescription" + static let dataDonation = "AppStrings.Settings.Datadonation.description" } enum AppInformation { diff --git a/src/xcode/ENA/ENA/Source/View Helpers/AppStrings.swift b/src/xcode/ENA/ENA/Source/View Helpers/AppStrings.swift index a871df71463..6a48fb6e107 100644 --- a/src/xcode/ENA/ENA/Source/View Helpers/AppStrings.swift +++ b/src/xcode/ENA/ENA/Source/View Helpers/AppStrings.swift @@ -439,6 +439,13 @@ enum AppStrings { static let navigationBarTitle = NSLocalizedString("Settings_NavTitle", comment: "") + enum Datadonation { + static let label = NSLocalizedString("Settings_DataDonation_Label", comment: "") + static let description = NSLocalizedString("Settings_DataDonation_Description", comment: "") + static let statusActive = NSLocalizedString("Settings_DataDonation_StatusActive", comment: "") + static let statusInactive = NSLocalizedString("Settings_DataDonation_StatusInactive", comment: "") + } + } enum NotificationSettings { @@ -1201,6 +1208,7 @@ enum AppStrings { } enum Info { + static let settingsSubHeadline = NSLocalizedString("DataDonation_SubHead_Settings", comment: "") static let accImageDescription = NSLocalizedString("DataDonation_AccImageDescription", comment: "") static let title = NSLocalizedString("DataDonation_Headline", comment: "") static let description = NSLocalizedString("DataDonation_Description", comment: "") @@ -1265,6 +1273,10 @@ enum AppStrings { static let bullet22_text = NSLocalizedString("DetailedInfosDataDonation_Misc_SubHead_BulletPoint_Region", comment: "") static let bullet23_text = NSLocalizedString("DetailedInfosDataDonation_Misc_SubHead_BulletPoint_TechSpecs", comment: "") } + + enum AppSettings { + static let ppaSettingsPrivacyInformationBody = NSLocalizedString("ppa_settings_privacy_information_body", tableName: "Localizable.legal", comment: "") + } } // swiftlint:disable:next file_length diff --git a/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift b/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift index a8aae9f39ba..ee05c318799 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Store/SecureStore.swift @@ -356,7 +356,15 @@ extension SecureStore: PrivacyPreservingProviding { var isPrivacyPreservingAnalyticsConsentGiven: Bool { get { kvStore["isPrivacyPreservingAnalyticsConsentGiven"] as Bool? ?? false } - set { kvStore["isPrivacyPreservingAnalyticsConsentGiven"] = newValue } + set { + kvStore["isPrivacyPreservingAnalyticsConsentGiven"] = newValue + currentRiskExposureMetadata = nil + previousRiskExposureMetadata = nil + userMetadata = nil + lastSubmittedPPAData = nil + lastAppReset = nil + lastSubmissionAnalytics = nil + } } var otpToken: OTPToken? { diff --git a/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift b/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift index 20f5883a29b..b82039568ec 100644 --- a/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift +++ b/src/xcode/ENA/ENA/Source/Workers/Store/Store.swift @@ -137,7 +137,7 @@ protocol StatisticsCaching: AnyObject { } protocol PrivacyPreservingProviding: AnyObject { - /// A boolean storing if the user has already confirmed to collect and submit the data for PPA + /// A boolean storing if the user has already confirmed to collect and submit the data for PPA. By setting it, the existing anlytics data will be reset. var isPrivacyPreservingAnalyticsConsentGiven: Bool { get set } /// OTP for user survey link generation var otpToken: OTPToken? { get set }