diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index c9d644ec3..5898fd95d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */; }; 0033481EE363E4914295F188 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C070FD43DC6BF4E50217965A /* LocalizationTests.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 0206016CCEF6EF9365916768 /* AppLockSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33284693F54382F46CFD2EDD /* AppLockSettingsScreenViewModelProtocol.swift */; }; @@ -64,6 +65,7 @@ 0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */; }; 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; + 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; 119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; }; @@ -133,6 +135,7 @@ 24BDDD09A90B8BFE3793F3AA /* ClientProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */; }; 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */; }; 256D76972BA3254F7CB7F88B /* LocationAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */; }; + 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */; }; 266C4DF893F2947DCCEF327B /* InvitesScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC14E5209C262530E19BC4C1 /* InvitesScreenViewModelTests.swift */; }; 2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BB243B26D54EF1A0C422C0 /* NotificationContentBuilder.swift */; }; 273AB64B9A26B61C51858867 /* AsyncSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73A07BAEDD74C48795A996A /* AsyncSequence.swift */; }; @@ -269,6 +272,7 @@ 4BAB8222DBA0B4207D1223E0 /* NotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */; }; 4BB282209EA82015D0DF8F89 /* NavigationStackCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */; }; 4BB51476A29E7E27BC14EA22 /* UserDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022E6BD64CB4610B9C95FC02 /* UserDetailsEditScreenViewModel.swift */; }; + 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */; }; 4C5A638DAA8AF64565BA4866 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5351EBD7A0B9610548E4B7B2 /* EncryptedRoomTimelineItem.swift */; }; 4E0D9E09B52CEC4C0E6211A8 /* MediaPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */; }; 4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948D16F021DFDB2CD26EAA8 /* MockBackgroundTaskService.swift */; }; @@ -282,6 +286,7 @@ 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; 4FFDC274824F7CC0BBDF581E /* BugReportScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */; }; 500CB65ED116B81DA52FDAEE /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 874A1842477895F199567BD7 /* TimelineView.swift */; }; + 50381244BA280451771BE3ED /* PINTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */; }; 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; }; 516534FC5C893D57F169D5A8 /* MapTilerGeocoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33720F7AD25E85E4A84669E8 /* MapTilerGeocoding.swift */; }; @@ -401,6 +406,7 @@ 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; + 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; @@ -468,6 +474,7 @@ 8421FFCD5360A15D170922A8 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A1D75C7C52CD14A327CC90 /* ProgressMaskModifier.swift */; }; 84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; }; 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; }; + 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */; }; 84CAE3E96D93194DA06B9194 /* CallScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */; }; 84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; 8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; }; @@ -543,6 +550,7 @@ 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 968823C9DBF3062729413EBF /* MigrationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DF0BFE5637EA42F5651FE8 /* MigrationScreenCoordinator.swift */; }; 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; + 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */; }; 97189E495F0E47805D1868DB /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */; }; 97969EF0B9C412CD38E5CA93 /* AppLockScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */; }; @@ -835,6 +843,7 @@ E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D99730313BEBF08CDE81EE3 /* EmojiDetection.swift */; }; E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; }; E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; }; + E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */; }; E4BAEED438A843D7B01D8069 /* CompletionSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F421E51DF00377DE1A01354 /* CompletionSuggestionView.swift */; }; E570117376826665640F0CFD /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B16CAF20C9AC874A210E2DCF /* SessionVerificationScreenViewModelProtocol.swift */; }; E571163060CBE87D82CE24FD /* NSESettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFC0FBA0FC6FC4DC0FC9FC /* NSESettings.swift */; }; @@ -1097,6 +1106,7 @@ 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = ""; }; 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = ""; }; 1F7C6DDBB5D12F6EF6A3D6E1 /* CollapsibleReactionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleReactionLayout.swift; sourceTree = ""; }; + 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenCoordinator.swift; sourceTree = ""; }; 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = ""; }; 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = ""; }; 20872C3887F835958CE2F1D0 /* MapTilerStaticMapProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStaticMapProtocol.swift; sourceTree = ""; }; @@ -1190,6 +1200,7 @@ 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; 3CCE3636E3D01477C8B2E9D0 /* ReportContentScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenModels.swift; sourceTree = ""; }; 3CFD5EB0B0EEA4549FB49784 /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; + 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextField.swift; sourceTree = ""; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; 3DC1943ADE6A62ED5129D7C8 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; @@ -1492,6 +1503,7 @@ 97CE98208321C4D66E363612 /* ShimmerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShimmerModifier.swift; sourceTree = ""; }; 981663D961C94270FA035FD0 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = ""; }; 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenUITests.swift; sourceTree = ""; }; + 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelTests.swift; sourceTree = ""; }; 98A2932515EA11D3DD8A3506 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = ""; }; 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModule.swift; sourceTree = ""; }; @@ -1510,6 +1522,7 @@ 9F3450F4C32D73532DBBC1A2 /* OnboardingScreenBackgroundImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreenBackgroundImage.swift; sourceTree = ""; }; 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; + A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; A057F2FDC14866C3026A89A4 /* NotificationManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerProtocol.swift; sourceTree = ""; }; A0A01AECFF54281CF35909A6 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; @@ -1565,6 +1578,7 @@ AE40D4A5DD857AC16EED945A /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableTask.swift; sourceTree = ""; }; AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; + AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenUITests.swift; sourceTree = ""; }; AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = ""; }; AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptUITests.swift; sourceTree = ""; }; @@ -1581,6 +1595,7 @@ B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; B3A1398EFF65090FDA1CB639 /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = ""; }; B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; + B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenModels.swift; sourceTree = ""; }; B43456E73F8A2D52B69B9FB9 /* TemplateScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModel.swift; sourceTree = ""; }; B48B7AD4908C5C374517B892 /* MapAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = MapAssets.xcassets; sourceTree = ""; }; B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; @@ -1593,6 +1608,7 @@ B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; B70A50C41C5871B4DB905E7E /* VoiceMessageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineView.swift; sourceTree = ""; }; B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsCustomSectionView.swift; sourceTree = ""; }; + B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelProtocol.swift; sourceTree = ""; }; B7AE92E7BFF71797BDE1D261 /* MapTilerStyleBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerStyleBuilder.swift; sourceTree = ""; }; B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenCoordinator.swift; sourceTree = ""; }; @@ -1706,6 +1722,7 @@ D38391154120264910D19528 /* PollMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollMock.swift; sourceTree = ""; }; D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreen.swift; sourceTree = ""; }; D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = ""; }; + D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreen.swift; sourceTree = ""; }; D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaSourceProxy.swift; sourceTree = ""; }; D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomTimelineItem.swift; sourceTree = ""; }; @@ -1788,6 +1805,7 @@ EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; EEAA2832D93EC7D2608703FB /* NSEUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEUserSession.swift; sourceTree = ""; }; + EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = ""; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = ""; }; EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewFrameReader.swift; sourceTree = ""; }; EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceTests.swift; sourceTree = ""; }; @@ -2144,6 +2162,7 @@ children = ( 3AD37D7DDF9904587601239D /* AppLockScreen */, CE39C9B97963CC30AB0859E5 /* AppLockSettingsScreen */, + 570026F1BA71A2D167652E48 /* AppLockSetupPINScreen */, ); path = AppLock; sourceTree = ""; @@ -2813,6 +2832,18 @@ path = Other; sourceTree = ""; }; + 570026F1BA71A2D167652E48 /* AppLockSetupPINScreen */ = { + isa = PBXGroup; + children = ( + 1FAF8C2226A57B9AB7446B31 /* AppLockSetupPINScreenCoordinator.swift */, + B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */, + A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */, + B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */, + AC24CD8D1E51188678C6AE07 /* View */, + ); + path = AppLockSetupPINScreen; + sourceTree = ""; + }; 593C7129C5927E25AD8B688F /* FlowCoordinators */ = { isa = PBXGroup; children = ( @@ -3536,6 +3567,7 @@ 7D0CBC76C80E04345E11F2DB /* Application.swift */, 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */, 5F088B61525099A48909743B /* AppLockSettingsScreenUITests.swift */, + AEDC38E389B89BCF5C1AFD4A /* AppLockSetupPINScreenUITests.swift */, 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */, C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */, 1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */, @@ -3607,7 +3639,9 @@ AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */, DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */, B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */, + 989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */, 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */, + EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */, ); path = AppLock; sourceTree = ""; @@ -3791,6 +3825,15 @@ path = View; sourceTree = ""; }; + AC24CD8D1E51188678C6AE07 /* View */ = { + isa = PBXGroup; + children = ( + D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */, + 3D1D4A6D451F43A03CACD01D /* PINTextField.swift */, + ); + path = View; + sourceTree = ""; + }; ACF39CFC617601C942702CDC /* Media */ = { isa = PBXGroup; children = ( @@ -4949,6 +4992,7 @@ 8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */, 77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */, 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */, + 25C4C1100B6EA79F5CC7CBB5 /* AppLockSetupPINScreenViewModelTests.swift in Sources */, 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */, EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */, 3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */, @@ -5001,6 +5045,7 @@ C11939FDC40716C4387275A4 /* NotificationSettingsEditScreenViewModelTests.swift in Sources */, E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */, 0C26A1588B17DCDE5F490FE3 /* OnboardingScreenViewModelTests.swift in Sources */, + 50381244BA280451771BE3ED /* PINTextFieldTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */, @@ -5104,6 +5149,11 @@ 0AD81E04A8C024C09B7AEAC5 /* AppLockSettingsScreenModels.swift in Sources */, A7BEE8216B4B12BE4C0F2C3F /* AppLockSettingsScreenViewModel.swift in Sources */, 0206016CCEF6EF9365916768 /* AppLockSettingsScreenViewModelProtocol.swift in Sources */, + 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */, + 9696ECAFB4F0C079C5C2A526 /* AppLockSetupPINScreenCoordinator.swift in Sources */, + E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */, + 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */, + 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */, EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */, 355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */, 12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */, @@ -5403,6 +5453,7 @@ 4714991754A08B58B4D7ED85 /* OnboardingScreenViewModelProtocol.swift in Sources */, 804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, + 847DE3A7EB9FCA2C429C6E85 /* PINTextField.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */, 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */, @@ -5707,6 +5758,7 @@ BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */, F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */, 61C345258DD392477E79A3B5 /* AppLockSettingsScreenUITests.swift in Sources */, + 000765812BABB81F5174C601 /* AppLockSetupPINScreenUITests.swift in Sources */, 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */, ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */, 7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index ff914fe63..adc53b2bb 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -77,6 +77,7 @@ "action_start_verification" = "Start verification"; "action_static_map_load" = "Tap to load map"; "action_take_photo" = "Take photo"; +"action_try_again" = "Try again"; "action_view_source" = "View source"; "action_yes" = "Yes"; "action.edit_poll" = "Edit poll"; @@ -273,6 +274,13 @@ "screen_app_lock_settings_remove_pin" = "Remove PIN"; "screen_app_lock_settings_remove_pin_alert_message" = "Are you sure you want to remove PIN?"; "screen_app_lock_settings_remove_pin_alert_title" = "Remove PIN?"; +"screen_app_lock_setup_choose_pin" = "Choose PIN"; +"screen_app_lock_setup_confirm_pin" = "Confirm PIN"; +"screen_app_lock_setup_pin_blacklisted_dialog_content" = "You cannot choose this as your PIN code for security reasons"; +"screen_app_lock_setup_pin_blacklisted_dialog_title" = "Choose a different PIN"; +"screen_app_lock_setup_pin_context" = "Lock %1$@ to add extra security to your chats.\n\nChoose something memorable. If you forget this PIN, you will be logged out of the app."; +"screen_app_lock_setup_pin_mismatch_dialog_content" = "Please enter the same PIN twice"; +"screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match"; "screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed"; "screen_app_lock_signout_alert_title" = "You are being signed out"; "screen_app_lock_subtitle" = "You have 3 attempts to unlock"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index e568eb360..631b76b83 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -174,6 +174,8 @@ public enum L10n { public static var actionStaticMapLoad: String { return L10n.tr("Localizable", "action_static_map_load") } /// Take photo public static var actionTakePhoto: String { return L10n.tr("Localizable", "action_take_photo") } + /// Try again + public static var actionTryAgain: String { return L10n.tr("Localizable", "action_try_again") } /// View source public static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") } /// Yes @@ -664,6 +666,24 @@ public enum L10n { public static var screenAppLockSettingsRemovePinAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_message") } /// Remove PIN? public static var screenAppLockSettingsRemovePinAlertTitle: String { return L10n.tr("Localizable", "screen_app_lock_settings_remove_pin_alert_title") } + /// Choose PIN + public static var screenAppLockSetupChoosePin: String { return L10n.tr("Localizable", "screen_app_lock_setup_choose_pin") } + /// Confirm PIN + public static var screenAppLockSetupConfirmPin: String { return L10n.tr("Localizable", "screen_app_lock_setup_confirm_pin") } + /// You cannot choose this as your PIN code for security reasons + public static var screenAppLockSetupPinBlacklistedDialogContent: String { return L10n.tr("Localizable", "screen_app_lock_setup_pin_blacklisted_dialog_content") } + /// Choose a different PIN + public static var screenAppLockSetupPinBlacklistedDialogTitle: String { return L10n.tr("Localizable", "screen_app_lock_setup_pin_blacklisted_dialog_title") } + /// Lock %1$@ to add extra security to your chats. + /// + /// Choose something memorable. If you forget this PIN, you will be logged out of the app. + public static func screenAppLockSetupPinContext(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_app_lock_setup_pin_context", String(describing: p1)) + } + /// Please enter the same PIN twice + public static var screenAppLockSetupPinMismatchDialogContent: String { return L10n.tr("Localizable", "screen_app_lock_setup_pin_mismatch_dialog_content") } + /// PINs don't match + public static var screenAppLockSetupPinMismatchDialogTitle: String { return L10n.tr("Localizable", "screen_app_lock_setup_pin_mismatch_dialog_title") } /// You’ll need to re-login and create a new PIN to proceed public static var screenAppLockSignoutAlertMessage: String { return L10n.tr("Localizable", "screen_app_lock_signout_alert_message") } /// You are being signed out diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index d02b2e5a3..b7d4a8a73 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -154,6 +154,27 @@ class AppLockServiceMock: AppLockServiceProtocol { return setupPINCodeReturnValue } } + //MARK: - validate + + var validateCallsCount = 0 + var validateCalled: Bool { + return validateCallsCount > 0 + } + var validateReceivedPinCode: String? + var validateReceivedInvocations: [String] = [] + var validateReturnValue: Result! + var validateClosure: ((String) -> Result)? + + func validate(_ pinCode: String) -> Result { + validateCallsCount += 1 + validateReceivedPinCode = pinCode + validateReceivedInvocations.append(pinCode) + if let validateClosure = validateClosure { + return validateClosure(pinCode) + } else { + return validateReturnValue + } + } //MARK: - disable var disableCallsCount = 0 diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift index 2582ba679..8afbaec20 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/View/AppLockScreen.swift @@ -60,6 +60,7 @@ struct AppLockScreen: View { } .font(.compound.bodyMDSemibold) } + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) .alert(item: $context.alertInfo) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift new file mode 100644 index 000000000..a3790f482 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift @@ -0,0 +1,70 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct AppLockSetupPINScreenCoordinatorParameters { + /// Whether the screen should start in create or unlock mode. + /// Specifying confirm here will raise a fatal error. + let initialMode: AppLockSetupPINScreenMode + let appLockService: AppLockServiceProtocol +} + +enum AppLockSetupPINScreenCoordinatorAction { + /// The user cancelled PIN entry. + case cancel + /// The user succeeded PIN entry. + case complete +} + +final class AppLockSetupPINScreenCoordinator: CoordinatorProtocol { + private let parameters: AppLockSetupPINScreenCoordinatorParameters + private var viewModel: AppLockSetupPINScreenViewModelProtocol + private let actionsSubject: PassthroughSubject = .init() + private var cancellables = Set() + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: AppLockSetupPINScreenCoordinatorParameters) { + guard parameters.initialMode != .confirm else { fatalError(".confirm is an invalid initial mode") } + + self.parameters = parameters + viewModel = AppLockSetupPINScreenViewModel(initialMode: parameters.initialMode, + appLockService: parameters.appLockService) + } + + func start() { + viewModel.actions.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .complete: + break + case .cancel: + break + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(AppLockSetupPINScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift new file mode 100644 index 000000000..e695476a0 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift @@ -0,0 +1,76 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum AppLockSetupPINScreenViewModelAction { + /// The user succeeded PIN entry. + case complete + /// The user cancelled PIN entry. + case cancel +} + +enum AppLockSetupPINScreenMode { + /// Creating a new PIN. + case create + /// Confirming the new PIN. + case confirm + /// Unlocking with the current PIN. + case unlock +} + +struct AppLockSetupPINScreenViewState: BindableState { + /// The current mode that the screen is in. + var mode: AppLockSetupPINScreenMode + + var title: String { + switch mode { + case .create: return L10n.screenAppLockSetupChoosePin + case .confirm: return L10n.screenAppLockSetupConfirmPin + case .unlock: return L10n.commonEnterYourPin + } + } + + var subtitle: String? { + switch mode { + case .create, .confirm: return L10n.screenAppLockSetupPinContext(InfoPlistReader.main.bundleDisplayName) + case .unlock: return nil + } + } + + var bindings: AppLockSetupPINScreenViewStateBindings +} + +struct AppLockSetupPINScreenViewStateBindings { + var pinCode: String + var alertInfo: AlertInfo? +} + +enum AppLockSetupPINScreenAlertType { + /// The user entered a weak PIN and it has been rejected. + case weakPIN + /// The user entered the wrong PIN when confirming the creation. + case pinMismatch + /// An error occurred setting the PIN code in the App Lock service. + case failedToSetPIN +} + +enum AppLockSetupPINScreenViewAction { + /// Confirm the entered PIN. + case submitPINCode + /// Stop entering a PIN. + case cancel +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift new file mode 100644 index 000000000..faf5a0cdc --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift @@ -0,0 +1,130 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias AppLockSetupPINScreenViewModelType = StateStoreViewModel + +class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLockSetupPINScreenViewModelProtocol { + private let appLockService: AppLockServiceProtocol + private var actionsSubject: PassthroughSubject = .init() + + /// The PIN entered by the user in `.create` mode, used for confirmation. + var newPIN: String? + + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(initialMode: AppLockSetupPINScreenMode, appLockService: AppLockServiceProtocol) { + self.appLockService = appLockService + super.init(initialViewState: AppLockSetupPINScreenViewState(mode: initialMode, bindings: .init(pinCode: ""))) + } + + // MARK: - Public + + override func process(viewAction: AppLockSetupPINScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .submitPINCode: + submitPINCode() + case .cancel: + actionsSubject.send(.cancel) + } + } + + // MARK: - Private + + /// Handle the entered PIN code. + private func submitPINCode() { + switch state.mode { + case .create: + createPIN() + case .confirm: + confirmPIN() + case .unlock: + unlock() + } + } + + /// Handles a PIN input from the create mode. Transitions to confirmation if valid. + private func createPIN() { + let pinCode = state.bindings.pinCode + if case let .failure(error) = appLockService.validate(pinCode) { + MXLog.warning("PIN rejected: \(error)") + handleError(.weakPIN) + return + } + + newPIN = pinCode + state.mode = .confirm + state.bindings.pinCode = "" + } + + /// Handles a PIN input from the confirm mode. Stores the pin if it matches. + private func confirmPIN() { + let pinCode = state.bindings.pinCode + guard pinCode == newPIN else { + MXLog.warning("PIN mismatch.") + handleError(.pinMismatch) + return + } + + if case let .failure(error) = appLockService.setupPINCode(pinCode) { + MXLog.warning("Failed to set PIN: \(error)") + if case .keychainError = error { + handleError(.failedToSetPIN) + return + } else { + handleError(.weakPIN) // Shouldn't really happen but just in case. + return + } + } + + actionsSubject.send(.complete) + } + + /// Handles a PIN input for the unlock mode. + private func unlock() { + guard appLockService.unlock(with: state.bindings.pinCode) else { + // show an error + // https://www.figma.com/file/0MMNu7cTOzLOlWb7ctTkv3?node-id=13067:107631&mode=dev#591068578 + state.bindings.pinCode = "" + return + } + + actionsSubject.send(.complete) + } + + private func handleError(_ error: AppLockSetupPINScreenAlertType) { + switch error { + case .weakPIN: + state.bindings.alertInfo = .init(id: error, + title: L10n.screenAppLockSetupPinBlacklistedDialogTitle, + message: L10n.screenAppLockSetupPinBlacklistedDialogContent, + primaryButton: .init(title: L10n.actionOk) { self.state.bindings.pinCode = "" }) + case .pinMismatch: + state.bindings.alertInfo = .init(id: error, + title: L10n.screenAppLockSetupPinMismatchDialogTitle, + message: L10n.screenAppLockSetupPinMismatchDialogContent, + primaryButton: .init(title: L10n.actionTryAgain) { self.state.bindings.pinCode = "" }) + case .failedToSetPIN: + state.bindings.alertInfo = .init(id: error) + } + } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModelProtocol.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModelProtocol.swift new file mode 100644 index 000000000..2c3dfc62c --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol AppLockSetupPINScreenViewModelProtocol { + var actions: AnyPublisher { get } + var context: AppLockSetupPINScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift new file mode 100644 index 000000000..6d817c169 --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/AppLockSetupPINScreen.swift @@ -0,0 +1,89 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import SwiftUI + +/// The screen shown to unlock the App Lock settings or to create a new PIN and enable the feature. +struct AppLockSetupPINScreen: View { + @ObservedObject var context: AppLockSetupPINScreenViewModel.Context + + var body: some View { + ScrollView { + VStack(spacing: 48) { + header + + PINTextField(pinCode: $context.pinCode, + isSecure: context.viewState.mode == .unlock) + .onChange(of: context.pinCode) { newValue in + guard newValue.count == 4 else { return } + context.send(viewAction: .submitPINCode) + } + } + .padding(.horizontal, 16) + .padding(.top, UIConstants.iconTopPaddingToNavigationBar) + .frame(maxWidth: .infinity) + } + .background(Color.compound.bgCanvasDefault.ignoresSafeArea()) + } + + var header: some View { + VStack(spacing: 8) { + HeroImage(image: Image(systemSymbol: .lock)) + .symbolVariant(.fill) + .padding(.bottom, 8) + + Text(context.viewState.title) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + + if let subtitle = context.viewState.subtitle { + Text(subtitle) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textSecondary) + } + } + } +} + +// MARK: - Previews + +struct AppLockSetupPINScreen_Previews: PreviewProvider, TestablePreview { + static let service = AppLockService(keychainController: KeychainControllerMock(), + appSettings: ServiceLocator.shared.settings) + static let createViewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: service) + static let confirmViewModel = AppLockSetupPINScreenViewModel(initialMode: .confirm, appLockService: service) + static let unlockViewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, appLockService: service) + + static var previews: some View { + NavigationStack { + AppLockSetupPINScreen(context: createViewModel.context) + } + .previewDisplayName("Create") + + NavigationStack { + AppLockSetupPINScreen(context: confirmViewModel.context) + } + .previewDisplayName("Confirm") + + NavigationStack { + AppLockSetupPINScreen(context: unlockViewModel.context) + } + .previewDisplayName("Unlock") + } +} diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift new file mode 100644 index 000000000..e7a23109c --- /dev/null +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/View/PINTextField.swift @@ -0,0 +1,125 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +/// A text field that enables secure entry of a numerical PIN code. +/// The view itself handles validation and the base text field type. +struct PINTextField: View { + @Binding var pinCode: String + var isSecure = false + + var body: some View { + textField + .textFieldStyle(PINTextFieldStyle(pinCode: pinCode, isSecure: isSecure)) + .keyboardType(.numberPad) + .onChange(of: pinCode) { newValue in + let sanitized = sanitize(newValue) + if sanitized != newValue { + MXLog.warning("PIN code input sanitized.") + pinCode = sanitized + } + } + } + + @ViewBuilder + var textField: some View { + if isSecure { + SecureField("", text: $pinCode) + } else { + TextField("", text: $pinCode) + } + } + + func sanitize(_ pinCode: String) -> String { + var sanitized = pinCode + if sanitized.count > 4 { sanitized = String(pinCode.prefix(4)) } + return sanitized.filter(\.isNumber) + } +} + +/// A text field style for displaying individual digits of a PIN code. +private struct PINTextFieldStyle: TextFieldStyle { + @FocusState private var isFocussed + + let pinCode: String + let isSecure: Bool + + public func _body(configuration: TextField<_Label>) -> some View { + HStack(spacing: 8) { + ForEach(0..<4) { index in + PINDigitField(digit: digit(index)) + } + } + .overlay { + configuration + .focused($isFocussed) + .opacity(0.0) + } + .onTapGesture { isFocussed = true } + } + + func digit(_ index: Int) -> Character? { + guard pinCode.count > index else { return nil } + let stringIndex = pinCode.index(pinCode.startIndex, offsetBy: index) + return isSecure ? "●" : pinCode[stringIndex] + } +} + +/// A single digit shown within the text field style. +private struct PINDigitField: View { + let digit: Character? + + var body: some View { + ZStack { + if let digit { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color.compound.bgSubtlePrimary) + Text(String(digit)) + .font(.compound.headingMDBold) + } else { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .inset(by: 0.5) + .stroke(Color.compound.iconPrimary, lineWidth: 1) + } + } + .frame(width: 48, height: 48) + } +} + +struct PINTextField_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + VStack { + PreviewWrapper(pinCode: "", isSecure: false) + PreviewWrapper(pinCode: "12", isSecure: false) + PreviewWrapper(pinCode: "1234", isSecure: false) + .padding(.bottom) + + PreviewWrapper(pinCode: "", isSecure: true) + PreviewWrapper(pinCode: "12", isSecure: true) + PreviewWrapper(pinCode: "1234", isSecure: true) + } + } + + struct PreviewWrapper: View { + @State var pinCode = "" + let isSecure: Bool + + var body: some View { + PINTextField(pinCode: $pinCode, isSecure: isSecure) + } + } +} diff --git a/ElementX/Sources/Services/AppLock/AppLockService.swift b/ElementX/Sources/Services/AppLock/AppLockService.swift index 29250f7f5..160527894 100644 --- a/ElementX/Sources/Services/AppLock/AppLockService.swift +++ b/ElementX/Sources/Services/AppLock/AppLockService.swift @@ -47,8 +47,8 @@ class AppLockService: AppLockServiceProtocol { } func setupPINCode(_ pinCode: String) -> Result { - guard validate(pinCode) else { return .failure(.invalidPIN) } - guard !appSettings.appLockPINCodeBlockList.contains(pinCode) else { return .failure(.weakPIN) } + let result = validate(pinCode) + guard case .success = result else { return result } do { try keychainController.setPINCode(pinCode) @@ -59,6 +59,12 @@ class AppLockService: AppLockServiceProtocol { } } + func validate(_ pinCode: String) -> Result { + guard pinCode.count == 4, pinCode.allSatisfy(\.isNumber) else { return .failure(.invalidPIN) } + guard !appSettings.appLockPINCodeBlockList.contains(pinCode) else { return .failure(.weakPIN) } + return .success(()) + } + func disable() { biometricUnlockEnabled = false keychainController.removePINCode() @@ -94,11 +100,6 @@ class AppLockService: AppLockServiceProtocol { } } - /// Ensures that a provided PIN code is long enough and only contains digits. - private func validate(_ pinCode: String) -> Bool { - pinCode.count == 4 && pinCode.allSatisfy(\.isNumber) - } - /// Shared logic for completing an unlock via a PIN or biometry. private func completeUnlock() -> Bool { timer.registerUnlock() diff --git a/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift b/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift index 2c20d22bc..a31d455c4 100644 --- a/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift +++ b/ElementX/Sources/Services/AppLock/AppLockServiceProtocol.swift @@ -36,6 +36,8 @@ protocol AppLockServiceProtocol: AnyObject { /// Sets the user's PIN code used to unlock the app. func setupPINCode(_ pinCode: String) -> Result + /// Validates the supplied PIN code is long enough, only contains digits and isn't a weak choice. + func validate(_ pinCode: String) -> Result /// Disables the App Lock feature, removing the user's stored PIN code. func disable() diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 33cee3d77..db5f79de7 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -161,6 +161,13 @@ class MockScreen: Identifiable { let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings) let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService)) return coordinator + case .appLockSetupFlow: + // Use the flow coordinator once more screens are added and remove the settings screen below. + let navigationStackCoordinator = NavigationStackCoordinator() + let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings) + let coordinator = AppLockSetupPINScreenCoordinator(parameters: .init(initialMode: .create, appLockService: appLockService)) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator case .appLockSettingsScreen: let navigationStackCoordinator = NavigationStackCoordinator() let appLockService = AppLockServiceMock.mock(biometryType: .faceID) diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index 28a90ce4e..642ef051f 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -30,6 +30,7 @@ enum UITestsScreenIdentifier: String { case migration case templateScreen case appLockScreen + case appLockSetupFlow case appLockSettingsScreen case home case settings diff --git a/UITests/Sources/AppLockSetupPINScreenUITests.swift b/UITests/Sources/AppLockSetupPINScreenUITests.swift new file mode 100644 index 000000000..a47d4613b --- /dev/null +++ b/UITests/Sources/AppLockSetupPINScreenUITests.swift @@ -0,0 +1,26 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import ElementX +import XCTest + +@MainActor +class AppLockSetupUITests: XCTestCase { + func testScreen() async throws { + let app = Application.launch(.appLockSetupFlow) + try await app.assertScreenshot(.appLockSetupFlow, step: 0) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSetupFlow-0.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSetupFlow-0.png new file mode 100644 index 000000000..ef1e9ce26 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.appLockSetupFlow-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4af88936877c1eb32530fe4eaaf0ec228f055a3c080766a15b81473752cdd53 +size 84142 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSetupFlow-0.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSetupFlow-0.png new file mode 100644 index 000000000..fddcf89c9 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.appLockSetupFlow-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ed1ba1d4a0f067dc463b8e122c81e910b57ce580c6d66b5aa66b9ec5239452 +size 91739 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSetupFlow-0.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSetupFlow-0.png new file mode 100644 index 000000000..3f8900c89 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.appLockSetupFlow-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb43f034b7fb94655b025e4ae9e6b652c27ebb6b4ab04fdae61c5958ab3d16c2 +size 101999 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSetupFlow-0.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSetupFlow-0.png new file mode 100644 index 000000000..2901e02c5 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.appLockSetupFlow-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d507ae7b47328770e3a82a730f00d9735a8d37da3df91818bcc1d76542b59325 +size 123595 diff --git a/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift new file mode 100644 index 000000000..03a712b9e --- /dev/null +++ b/UnitTests/Sources/AppLock/AppLockSetupPINScreenViewModelTests.swift @@ -0,0 +1,96 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +@MainActor +class AppLockSetupPINScreenViewModelTests: XCTestCase { + var appLockService: AppLockService! + var keychainController: KeychainControllerMock! + var viewModel: AppLockSetupPINScreenViewModelProtocol! + + var context: AppLockSetupPINScreenViewModelType.Context { viewModel.context } + + override func setUp() { + AppSettings.reset() + keychainController = KeychainControllerMock() + appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings()) + } + + override func tearDown() { + AppSettings.reset() + } + + func testCreatePIN() async throws { + viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) + XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") + + let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm } + context.pinCode = "2023" + context.send(viewAction: .submitPINCode) + try await createDeferred.fulfill() + XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.") + + let confirmDeferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete } + context.pinCode = "2023" + context.send(viewAction: .submitPINCode) + try await confirmDeferred.fulfill() + } + + func testCreateWeakPIN() async throws { + viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) + XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") + XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") + + context.pinCode = "0000" + context.send(viewAction: .submitPINCode) + + XCTAssertEqual(context.alertInfo?.id, .weakPIN, "The weak PIN should be rejected.") + XCTAssertEqual(context.viewState.mode, .create, "The mode shouldn't transition after an invalid PIN code.") + } + + func testCreatePINMismatch() async throws { + viewModel = AppLockSetupPINScreenViewModel(initialMode: .create, appLockService: appLockService) + XCTAssertEqual(context.viewState.mode, .create, "The mode should start as creation.") + XCTAssertNil(context.alertInfo, "There shouldn't be an alert to begin with.") + + let createDeferred = deferFulfillment(context.$viewState, message: "A valid PIN needs confirming.") { $0.mode == .confirm } + context.pinCode = "2023" + context.send(viewAction: .submitPINCode) + try await createDeferred.fulfill() + XCTAssertEqual(context.viewState.mode, .confirm, "The mode should transition to confirmation.") + XCTAssertNil(context.alertInfo, "There shouldn't be an alert after a valid initial PIN.") + + context.pinCode = "2024" + context.send(viewAction: .submitPINCode) + + XCTAssertEqual(context.alertInfo?.id, .pinMismatch, "A PIN mismatch should be rejected.") + } + + func testUnlock() async throws { + viewModel = AppLockSetupPINScreenViewModel(initialMode: .unlock, appLockService: appLockService) + let pinCode = "2023" + keychainController.pinCodeReturnValue = pinCode + keychainController.containsPINCodeReturnValue = true + + let deferred = deferFulfillment(viewModel.actions, message: "The screen should be finished.") { $0 == .complete } + context.pinCode = pinCode + context.send(viewAction: .submitPINCode) + try await deferred.fulfill() + } +} diff --git a/UnitTests/Sources/AppLock/PINTextFieldTests.swift b/UnitTests/Sources/AppLock/PINTextFieldTests.swift new file mode 100644 index 000000000..17b14b11b --- /dev/null +++ b/UnitTests/Sources/AppLock/PINTextFieldTests.swift @@ -0,0 +1,31 @@ +// +// Copyright 2023 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +class PINTextFieldTests: XCTestCase { + func testSanitize() { + let textField = PINTextField(pinCode: .constant("")) + XCTAssertEqual(textField.sanitize("2"), "2") + XCTAssertEqual(textField.sanitize("2023"), "2023") + XCTAssertEqual(textField.sanitize("20233"), "2023") + XCTAssertEqual(textField.sanitize("20x"), "20") + XCTAssertEqual(textField.sanitize("20!"), "20") + XCTAssertEqual(textField.sanitize("boop"), "") + } +} diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Confirm.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Confirm.png new file mode 100644 index 000000000..f267b11ab --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Confirm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c78cada10d7c15137a635b0e841fe20e124814921d953296fca5ed16dea93fef +size 98997 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Create.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Create.png new file mode 100644 index 000000000..40d87b8d9 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Create.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2aa8303cb959ebeae8a6b6bced04f2c3ecdfe429c97f153a38a2d650dddadf4 +size 99386 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Unlock.png b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Unlock.png new file mode 100644 index 000000000..34b1777a4 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_appLockSetupPINScreen.Unlock.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e53aa446554e19bc47b31f59038ec3caeeaba8c4c15a1884f5e510f30eac4c61 +size 72867 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_pINTextField.1.png b/UnitTests/__Snapshots__/PreviewTests/test_pINTextField.1.png new file mode 100644 index 000000000..f7f3d3c0c --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_pINTextField.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcab4fcacf9a140239e887d4133d281d7defdd89a8ab6e6f3a2f5fba8815fde8 +size 98245 diff --git a/changelog.d/pr-1930.wip b/changelog.d/pr-1930.wip new file mode 100644 index 000000000..83992a1b6 --- /dev/null +++ b/changelog.d/pr-1930.wip @@ -0,0 +1 @@ +Add PIN entry screen for creating/accessing PIN settings. \ No newline at end of file