mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Initial service implementation for using a PIN code. (#1912)
* Initial service implementation for using a PIN code. * Tweak Danger for commit size 600-800 lines is perfectly normal for our PRs, up it to 1000.
This commit is contained in:
parent
fbcf037240
commit
99c28784a9
@ -6,7 +6,7 @@ SwiftLint.lint(inline: true)
|
|||||||
let danger = Danger()
|
let danger = Danger()
|
||||||
|
|
||||||
// Warn when there is a big PR
|
// Warn when there is a big PR
|
||||||
if (danger.github.pullRequest.additions ?? 0) > 500 {
|
if (danger.github.pullRequest.additions ?? 0) > 1000 {
|
||||||
warn("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
|
warn("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
0EAEA507586717B055441970 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80AD634BF0A1767FE8941C5 /* AppLockScreenCoordinator.swift */; };
|
0EAEA507586717B055441970 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C80AD634BF0A1767FE8941C5 /* AppLockScreenCoordinator.swift */; };
|
||||||
0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */; };
|
0ED691ADC9C2EA457E7A9427 /* FormattingToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE449DFBA7CC863EEB2FD2A /* FormattingToolbar.swift */; };
|
||||||
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; };
|
0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; };
|
||||||
|
0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; };
|
||||||
0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; };
|
0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */; };
|
||||||
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; };
|
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; };
|
||||||
119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; };
|
119AE9A3FC6E0606C1146528 /* NotificationSettingsEditScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97F8963B14EB0AF3940DDBF /* NotificationSettingsEditScreenRoomCell.swift */; };
|
||||||
@ -408,6 +409,7 @@
|
|||||||
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
7708976CEE6AFB5CFAEFBA68 /* PillTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CF1EE0AA78470C674554262 /* PillTextAttachment.swift */; };
|
||||||
7719778A682FDAC21445E9C8 /* OnboardingLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */; };
|
7719778A682FDAC21445E9C8 /* OnboardingLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0D7955FFB19B584594844B /* OnboardingLogo.swift */; };
|
||||||
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
|
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */; };
|
||||||
|
77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */; };
|
||||||
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
77920AFA8091AC6B9F190C90 /* Signposter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752A0EB49BF5BCEA37EDF7A3 /* Signposter.swift */; };
|
||||||
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */; };
|
77BB228AEA861E50FFD6A228 /* HomeScreenEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FEA560929DD73FFEF8C3DF /* HomeScreenEmptyStateView.swift */; };
|
||||||
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */; };
|
77C1A2F49CD90D3EFDF376E5 /* MapTilerURLBuildersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */; };
|
||||||
@ -451,6 +453,7 @@
|
|||||||
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
|
83A4DAB181C56987C3E804FF /* MapTilerStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B9F5BC4C80543DE7228B9D /* MapTilerStyle.swift */; };
|
||||||
8421FFCD5360A15D170922A8 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A1D75C7C52CD14A327CC90 /* ProgressMaskModifier.swift */; };
|
8421FFCD5360A15D170922A8 /* ProgressMaskModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A1D75C7C52CD14A327CC90 /* ProgressMaskModifier.swift */; };
|
||||||
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
|
84226AD2E1F1FBC965F3B09E /* UnitTestsAppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A8E19C4645D3F5F9FB02355 /* UnitTestsAppCoordinator.swift */; };
|
||||||
|
8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */; };
|
||||||
84CAE3E96D93194DA06B9194 /* CallScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */; };
|
84CAE3E96D93194DA06B9194 /* CallScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD9AD6AE5FC868962F090740 /* CallScreenViewModelProtocol.swift */; };
|
||||||
84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
|
84EFCB95F9DA2979C8042B26 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; };
|
||||||
8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; };
|
8544657DEEE717ED2E22E382 /* RoomNotificationSettingsProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D1BAA90F3A073D91B4F16B /* RoomNotificationSettingsProxyMock.swift */; };
|
||||||
@ -603,7 +606,6 @@
|
|||||||
A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */; };
|
A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */; };
|
||||||
A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */; };
|
A9D349478F7D4A2B1E40CEF9 /* LegalInformationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8977176AB534AA41630395BC /* LegalInformationScreenViewModelProtocol.swift */; };
|
||||||
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; };
|
AA050DF4AEE54A641BA7CA22 /* RoomSummaryProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */; };
|
||||||
AA64AAE1C4BB96C7F2761CAB /* AppLockScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */; };
|
|
||||||
AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; };
|
AA93B3F9B5DD097DEF79F981 /* NotificationSettingsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB0328F2887BF0A65BC5D49 /* NotificationSettingsEditScreen.swift */; };
|
||||||
AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; };
|
AAF0BBED840DF4A53EE85E77 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C2C69B8BA5A9702E7A8BC08F /* MatrixRustSDK */; };
|
||||||
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; };
|
ABF3FAB234AD3565B214309B /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */; };
|
||||||
@ -846,6 +848,7 @@
|
|||||||
EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; };
|
EF0D0155DD104C7A41A2EB0E /* PlainMentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */; };
|
||||||
EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; };
|
EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; };
|
||||||
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; };
|
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; };
|
||||||
|
EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */; };
|
||||||
F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */; };
|
F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */; };
|
||||||
F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; };
|
F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; };
|
||||||
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
|
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
|
||||||
@ -1186,6 +1189,7 @@
|
|||||||
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = "<group>"; };
|
||||||
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = "<group>"; };
|
||||||
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
|
47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimer.swift; sourceTree = "<group>"; };
|
||||||
4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
4959CECEC984B3995616F427 /* DataProtectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProtectionManager.swift; sourceTree = "<group>"; };
|
||||||
4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenModels.swift; sourceTree = "<group>"; };
|
4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenModels.swift; sourceTree = "<group>"; };
|
||||||
49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = "<group>"; };
|
49ABAB186CF00B15C5521D04 /* MenuSheetLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSheetLabelStyle.swift; sourceTree = "<group>"; };
|
||||||
@ -1194,6 +1198,7 @@
|
|||||||
49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||||
4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
||||||
|
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
|
||||||
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
||||||
4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = "<group>"; };
|
4ADC55DFF46083BC957E0019 /* CreatePollScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatePollScreenModels.swift; sourceTree = "<group>"; };
|
||||||
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = "<group>"; };
|
||||||
@ -1206,7 +1211,6 @@
|
|||||||
4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = "<group>"; };
|
4E47F18A9A077E351CEA10D4 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = "<group>"; };
|
||||||
4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = "<group>"; };
|
4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxyProtocol.swift; sourceTree = "<group>"; };
|
||||||
4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = "<group>"; };
|
|
||||||
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; };
|
4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinatorTests.swift; sourceTree = "<group>"; };
|
||||||
4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
|
4FD6E621CC5E6D4830D96D2D /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = "<group>"; };
|
||||||
4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
|
4FDD775CFD72DD2D3C8A8390 /* NotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
|
||||||
@ -1479,6 +1483,7 @@
|
|||||||
AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = "<group>"; };
|
AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTaskServiceProtocol.swift; sourceTree = "<group>"; };
|
||||||
AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
|
AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingScreen.swift; sourceTree = "<group>"; };
|
||||||
ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = "<group>"; };
|
AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = "<group>"; };
|
||||||
AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
AC3F82523D6F48B926D6AF68 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||||
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerTests.swift; sourceTree = "<group>"; };
|
AC4F10BDD56FA77FEC742333 /* VoiceMessageMediaManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManagerTests.swift; sourceTree = "<group>"; };
|
||||||
@ -1661,6 +1666,7 @@
|
|||||||
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
|
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
|
||||||
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
|
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
|
||||||
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceTests.swift; sourceTree = "<group>"; };
|
||||||
DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenUITests.swift; sourceTree = "<group>"; };
|
DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenUITests.swift; sourceTree = "<group>"; };
|
||||||
DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = "<group>"; };
|
DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = "<group>"; };
|
||||||
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = "<group>"; };
|
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
@ -2904,7 +2910,6 @@
|
|||||||
children = (
|
children = (
|
||||||
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */,
|
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */,
|
||||||
C687844F60BFF532D49A994C /* AnalyticsTests.swift */,
|
C687844F60BFF532D49A994C /* AnalyticsTests.swift */,
|
||||||
4F4BEE95A091150EEBF1C358 /* AppLockScreenViewModelTests.swift */,
|
|
||||||
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */,
|
E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */,
|
||||||
893777A4997BBDB68079D4F5 /* ArrayTests.swift */,
|
893777A4997BBDB68079D4F5 /* ArrayTests.swift */,
|
||||||
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */,
|
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */,
|
||||||
@ -2978,6 +2983,7 @@
|
|||||||
C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */,
|
C796FC1DFDBCDD5573D0360F /* WaitlistScreenViewModelTests.swift */,
|
||||||
851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */,
|
851EF6258DF8B7EF129DC3AC /* WelcomeScreenScreenViewModelTests.swift */,
|
||||||
53280D2292E6C9C7821773FD /* UserSession */,
|
53280D2292E6C9C7821773FD /* UserSession */,
|
||||||
|
9613851C68D8C01EABFB3569 /* AppLock */,
|
||||||
70C5B842301AC281DF374E41 /* Extensions */,
|
70C5B842301AC281DF374E41 /* Extensions */,
|
||||||
A6AA0A048CAE428A5CA4CBBB /* LayoutTests */,
|
A6AA0A048CAE428A5CA4CBBB /* LayoutTests */,
|
||||||
7583EAC171059A86B767209F /* MediaProvider */,
|
7583EAC171059A86B767209F /* MediaProvider */,
|
||||||
@ -3045,6 +3051,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
851B95BB98649B8E773D6790 /* AppLockService.swift */,
|
851B95BB98649B8E773D6790 /* AppLockService.swift */,
|
||||||
|
490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */,
|
||||||
);
|
);
|
||||||
path = AppLock;
|
path = AppLock;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -3448,6 +3455,16 @@
|
|||||||
path = TimelineItems;
|
path = TimelineItems;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
9613851C68D8C01EABFB3569 /* AppLock */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */,
|
||||||
|
DD97F9661ABF08CE002054A2 /* AppLockServiceTests.swift */,
|
||||||
|
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */,
|
||||||
|
);
|
||||||
|
path = AppLock;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
99B9B46F2D621380428E68F7 /* ElementX */ = {
|
99B9B46F2D621380428E68F7 /* ElementX */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -4739,7 +4756,9 @@
|
|||||||
files = (
|
files = (
|
||||||
A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */,
|
A9A5801D5EE3D4D91F6DDADB /* AnalyticsSettingsScreenViewModelTests.swift in Sources */,
|
||||||
890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */,
|
890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */,
|
||||||
AA64AAE1C4BB96C7F2761CAB /* AppLockScreenViewModelTests.swift in Sources */,
|
8478992479B296C45150208F /* AppLockScreenViewModelTests.swift in Sources */,
|
||||||
|
77693820498ABF3508814D49 /* AppLockServiceTests.swift in Sources */,
|
||||||
|
0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */,
|
||||||
EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */,
|
EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */,
|
||||||
3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */,
|
3EC698F80DDEEFA273857841 /* ArrayTests.swift in Sources */,
|
||||||
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */,
|
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */,
|
||||||
@ -4884,6 +4903,7 @@
|
|||||||
BE641CE5F9036B9AD7367DF1 /* AppLockScreenViewModel.swift in Sources */,
|
BE641CE5F9036B9AD7367DF1 /* AppLockScreenViewModel.swift in Sources */,
|
||||||
33094DB91C3A4131E76B2C07 /* AppLockScreenViewModelProtocol.swift in Sources */,
|
33094DB91C3A4131E76B2C07 /* AppLockScreenViewModelProtocol.swift in Sources */,
|
||||||
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */,
|
1D623953F970D11F6F38499C /* AppLockService.swift in Sources */,
|
||||||
|
EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */,
|
||||||
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */,
|
355B11D08CE0CEF97A813236 /* AppRoutes.swift in Sources */,
|
||||||
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */,
|
12CCA59536EDD99A3272CF77 /* AppSettings.swift in Sources */,
|
||||||
9462C62798F47E39DCC182D2 /* Application.swift in Sources */,
|
9462C62798F47E39DCC182D2 /* Application.swift in Sources */,
|
||||||
|
@ -117,6 +117,13 @@ final class AppSettings {
|
|||||||
/// An email address that should be used for support requests.
|
/// An email address that should be used for support requests.
|
||||||
let supportEmailAddress = "support@element.io"
|
let supportEmailAddress = "support@element.io"
|
||||||
|
|
||||||
|
// MARK: - Security
|
||||||
|
|
||||||
|
/// The amount of time the app can remain in the background for without requesting the PIN/TouchID/FaceID.
|
||||||
|
let appLockGracePeriod: TimeInterval = 180
|
||||||
|
/// Any codes that the user isn't allowed to use for their PIN.
|
||||||
|
let appLockPINCodeBlockList = ["0000", "1234"]
|
||||||
|
|
||||||
// MARK: - Authentication
|
// MARK: - Authentication
|
||||||
|
|
||||||
/// The URL that is opened when tapping the Learn more button on the sliding sync alert during authentication.
|
/// The URL that is opened when tapping the Learn more button on the sliding sync alert during authentication.
|
||||||
|
@ -42,13 +42,13 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
|
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.showPlaceholderIfNeeded()
|
self?.applicationDidEnterBackground()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.showUnlockScreenIfNeeded()
|
self?.applicationWillEnterForeground()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@ -59,18 +59,26 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - App unlock
|
// MARK: - App unlock
|
||||||
|
|
||||||
/// Displays the unlock flow with the app's placeholder view to hide obscure the view hierarchy in the app switcher.
|
private func applicationDidEnterBackground() {
|
||||||
private func showPlaceholderIfNeeded() {
|
|
||||||
guard appLockService.isEnabled else { return }
|
guard appLockService.isEnabled else { return }
|
||||||
|
|
||||||
|
appLockService.applicationDidEnterBackground()
|
||||||
|
showPlaceholder()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applicationWillEnterForeground() {
|
||||||
|
guard appLockService.isEnabled, appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) else { return }
|
||||||
|
showUnlockScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays the unlock flow with the app's placeholder view to hide obscure the view hierarchy in the app switcher.
|
||||||
|
private func showPlaceholder() {
|
||||||
navigationCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(), animated: false)
|
navigationCoordinator.setRootCoordinator(PlaceholderScreenCoordinator(), animated: false)
|
||||||
actionsSubject.send(.lockApp)
|
actionsSubject.send(.lockApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Displays the unlock flow with the main unlock screen.
|
/// Displays the unlock flow with the main unlock screen.
|
||||||
private func showUnlockScreenIfNeeded() {
|
private func showUnlockScreen() {
|
||||||
guard appLockService.isEnabled, appLockService.needsUnlock else { return }
|
|
||||||
|
|
||||||
let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService))
|
let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService))
|
||||||
coordinator.actions.sink { [weak self] action in
|
coordinator.actions.sink { [weak self] action in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
@ -564,6 +564,88 @@ class KeychainControllerMock: KeychainControllerProtocol {
|
|||||||
removeAllRestorationTokensCallsCount += 1
|
removeAllRestorationTokensCallsCount += 1
|
||||||
removeAllRestorationTokensClosure?()
|
removeAllRestorationTokensClosure?()
|
||||||
}
|
}
|
||||||
|
//MARK: - resetSecrets
|
||||||
|
|
||||||
|
var resetSecretsCallsCount = 0
|
||||||
|
var resetSecretsCalled: Bool {
|
||||||
|
return resetSecretsCallsCount > 0
|
||||||
|
}
|
||||||
|
var resetSecretsClosure: (() -> Void)?
|
||||||
|
|
||||||
|
func resetSecrets() {
|
||||||
|
resetSecretsCallsCount += 1
|
||||||
|
resetSecretsClosure?()
|
||||||
|
}
|
||||||
|
//MARK: - containsPINCode
|
||||||
|
|
||||||
|
var containsPINCodeThrowableError: Error?
|
||||||
|
var containsPINCodeCallsCount = 0
|
||||||
|
var containsPINCodeCalled: Bool {
|
||||||
|
return containsPINCodeCallsCount > 0
|
||||||
|
}
|
||||||
|
var containsPINCodeReturnValue: Bool!
|
||||||
|
var containsPINCodeClosure: (() throws -> Bool)?
|
||||||
|
|
||||||
|
func containsPINCode() throws -> Bool {
|
||||||
|
if let error = containsPINCodeThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
containsPINCodeCallsCount += 1
|
||||||
|
if let containsPINCodeClosure = containsPINCodeClosure {
|
||||||
|
return try containsPINCodeClosure()
|
||||||
|
} else {
|
||||||
|
return containsPINCodeReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - setPINCode
|
||||||
|
|
||||||
|
var setPINCodeThrowableError: Error?
|
||||||
|
var setPINCodeCallsCount = 0
|
||||||
|
var setPINCodeCalled: Bool {
|
||||||
|
return setPINCodeCallsCount > 0
|
||||||
|
}
|
||||||
|
var setPINCodeReceivedPinCode: String?
|
||||||
|
var setPINCodeReceivedInvocations: [String] = []
|
||||||
|
var setPINCodeClosure: ((String) throws -> Void)?
|
||||||
|
|
||||||
|
func setPINCode(_ pinCode: String) throws {
|
||||||
|
if let error = setPINCodeThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
setPINCodeCallsCount += 1
|
||||||
|
setPINCodeReceivedPinCode = pinCode
|
||||||
|
setPINCodeReceivedInvocations.append(pinCode)
|
||||||
|
try setPINCodeClosure?(pinCode)
|
||||||
|
}
|
||||||
|
//MARK: - pinCode
|
||||||
|
|
||||||
|
var pinCodeCallsCount = 0
|
||||||
|
var pinCodeCalled: Bool {
|
||||||
|
return pinCodeCallsCount > 0
|
||||||
|
}
|
||||||
|
var pinCodeReturnValue: String?
|
||||||
|
var pinCodeClosure: (() -> String?)?
|
||||||
|
|
||||||
|
func pinCode() -> String? {
|
||||||
|
pinCodeCallsCount += 1
|
||||||
|
if let pinCodeClosure = pinCodeClosure {
|
||||||
|
return pinCodeClosure()
|
||||||
|
} else {
|
||||||
|
return pinCodeReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - removePINCode
|
||||||
|
|
||||||
|
var removePINCodeCallsCount = 0
|
||||||
|
var removePINCodeCalled: Bool {
|
||||||
|
return removePINCodeCallsCount > 0
|
||||||
|
}
|
||||||
|
var removePINCodeClosure: (() -> Void)?
|
||||||
|
|
||||||
|
func removePINCode() {
|
||||||
|
removePINCodeCallsCount += 1
|
||||||
|
removePINCodeClosure?()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
class MediaPlayerMock: MediaPlayerProtocol {
|
class MediaPlayerMock: MediaPlayerProtocol {
|
||||||
var mediaSource: MediaSourceProxy?
|
var mediaSource: MediaSourceProxy?
|
||||||
|
@ -16,14 +16,33 @@
|
|||||||
|
|
||||||
import LocalAuthentication
|
import LocalAuthentication
|
||||||
|
|
||||||
|
enum AppLockServiceError: Error {
|
||||||
|
/// The operation failed to access the keychain.
|
||||||
|
case keychainError
|
||||||
|
/// The PIN code was rejected because it isn't long enough, or contains invalid characters.
|
||||||
|
case invalidPIN
|
||||||
|
/// The PIN code was rejected as an insecure choice.
|
||||||
|
case weakPIN
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol AppLockServiceProtocol {
|
protocol AppLockServiceProtocol {
|
||||||
/// The app has been configured to automatically lock with a PIN code.
|
/// The app has been configured to automatically lock with a PIN code.
|
||||||
var isEnabled: Bool { get }
|
var isEnabled: Bool { get }
|
||||||
/// The app can additionally be unlocked using FaceID or TouchID.
|
/// The type of biometric authentication supported by the device.
|
||||||
var supportsBiometrics: Bool { get }
|
var biometryType: LABiometryType { get }
|
||||||
/// The app should be unlocked with a PIN code/biometrics before being presented.
|
/// Whether or not the user has enabled unlock via TouchID, FaceID or (possibly) OpticID.
|
||||||
var needsUnlock: Bool { get }
|
var biometricUnlockEnabled: Bool { get set }
|
||||||
|
|
||||||
|
/// Sets the user's PIN code used to unlock the app.
|
||||||
|
func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError>
|
||||||
|
/// Disables the App Lock feature, removing the user's stored PIN code.
|
||||||
|
func disable()
|
||||||
|
|
||||||
|
/// Informs the service that the app has entered the background.
|
||||||
|
func applicationDidEnterBackground()
|
||||||
|
/// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding.
|
||||||
|
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool
|
||||||
|
|
||||||
/// Attempt to unlock the app with the supplied PIN code.
|
/// Attempt to unlock the app with the supplied PIN code.
|
||||||
func unlock(with pinCode: String) -> Bool
|
func unlock(with pinCode: String) -> Bool
|
||||||
@ -31,25 +50,80 @@ protocol AppLockServiceProtocol {
|
|||||||
func unlockWithBiometrics() -> Bool
|
func unlockWithBiometrics() -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The service responsible for locking and unlocking the app.
|
||||||
class AppLockService: AppLockServiceProtocol {
|
class AppLockService: AppLockServiceProtocol {
|
||||||
private let keychainController: KeychainControllerProtocol
|
private let keychainController: KeychainControllerProtocol
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
|
private let context = LAContext()
|
||||||
|
|
||||||
var isEnabled: Bool { appSettings.appLockFlowEnabled }
|
private let timer: AppLockTimer
|
||||||
var supportsBiometrics: Bool { true }
|
|
||||||
var needsUnlock: Bool { true }
|
var isEnabled: Bool {
|
||||||
|
do {
|
||||||
|
guard appSettings.appLockFlowEnabled else { return false }
|
||||||
|
return try keychainController.containsPINCode()
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Keychain access error: \(error)")
|
||||||
|
MXLog.error("Locking the app.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var biometryType: LABiometryType { context.biometryType }
|
||||||
|
var biometricUnlockEnabled = false // Needs to be stored, not sure if in the keychain or defaults yet.
|
||||||
|
|
||||||
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) {
|
init(keychainController: KeychainControllerProtocol, appSettings: AppSettings) {
|
||||||
self.keychainController = keychainController
|
self.keychainController = keychainController
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
|
timer = AppLockTimer(gracePeriod: appSettings.appLockGracePeriod)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupPINCode(_ pinCode: String) -> Result<Void, AppLockServiceError> {
|
||||||
|
guard validate(pinCode) else { return .failure(.invalidPIN) }
|
||||||
|
guard !appSettings.appLockPINCodeBlockList.contains(pinCode) else { return .failure(.weakPIN) }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try keychainController.setPINCode(pinCode)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Keychain access error: \(error)")
|
||||||
|
return .failure(.keychainError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() {
|
||||||
|
biometricUnlockEnabled = false
|
||||||
|
keychainController.removePINCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationDidEnterBackground() {
|
||||||
|
timer.applicationDidEnterBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
|
||||||
|
timer.computeLockState(willEnterForegroundAt: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unlock(with pinCode: String) -> Bool {
|
func unlock(with pinCode: String) -> Bool {
|
||||||
true
|
guard pinCode == keychainController.pinCode() else { return false }
|
||||||
|
return completeUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func unlockWithBiometrics() -> Bool {
|
func unlockWithBiometrics() -> Bool {
|
||||||
guard supportsBiometrics else { return false }
|
guard biometryType != .none, biometricUnlockEnabled else { return false }
|
||||||
|
return completeUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// 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()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
ElementX/Sources/Services/AppLock/AppLockTimer.swift
Normal file
62
ElementX/Sources/Services/AppLock/AppLockTimer.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// A timer that adds a grace-period to the app before locking it.
|
||||||
|
class AppLockTimer {
|
||||||
|
/// The amount of time the app should remain unlocked for whilst backgrounded.
|
||||||
|
let gracePeriod: TimeInterval
|
||||||
|
|
||||||
|
/// Whether the timer considers the app to be locked or not.
|
||||||
|
///
|
||||||
|
/// Internally this value may be incorrect, always call `needsUnlock` to get the correct value.
|
||||||
|
private var isLocked = true
|
||||||
|
/// The date when the app was last backgrounded whilst in an unlocked state.
|
||||||
|
private var lastUnlockedBackground: Date?
|
||||||
|
|
||||||
|
/// Creates a new timer.
|
||||||
|
/// - Parameter gracePeriod: The amount of time the app should remain unlocked for whilst backgrounded.
|
||||||
|
init(gracePeriod: TimeInterval) {
|
||||||
|
self.gracePeriod = 180
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signals to the timer to track how long the app will be backgrounded for.
|
||||||
|
func applicationDidEnterBackground(date: Date = .now) {
|
||||||
|
// Only update the last background date if the app is currently unlocked.
|
||||||
|
guard !isLocked else { return }
|
||||||
|
lastUnlockedBackground = date
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asks the timer to recompute the lock state on foregrounding.
|
||||||
|
func computeLockState(willEnterForegroundAt date: Date) -> Bool {
|
||||||
|
guard !isLocked, let lastUnlockedBackground else { return true }
|
||||||
|
|
||||||
|
isLocked = date.timeIntervalSince(lastUnlockedBackground) >= gracePeriod
|
||||||
|
|
||||||
|
// Don't allow changing the device's clock to unlock the app.
|
||||||
|
if date < lastUnlockedBackground {
|
||||||
|
isLocked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a successful unlock with the timer.
|
||||||
|
func registerUnlock() {
|
||||||
|
isLocked = false
|
||||||
|
}
|
||||||
|
}
|
@ -22,24 +22,36 @@ enum KeychainControllerService: String {
|
|||||||
case sessions
|
case sessions
|
||||||
case tests
|
case tests
|
||||||
|
|
||||||
var identifier: String {
|
var restorationTokenID: String {
|
||||||
InfoPlistReader.main.baseBundleIdentifier + "." + rawValue
|
InfoPlistReader.main.baseBundleIdentifier + "." + rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mainID: String {
|
||||||
|
InfoPlistReader.main.baseBundleIdentifier + ".keychain.\(rawValue)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeychainController: KeychainControllerProtocol {
|
class KeychainController: KeychainControllerProtocol {
|
||||||
private let keychain: Keychain
|
/// The keychain responsible for storing account restoration tokens (keyed by userID).
|
||||||
|
private let restorationTokenKeychain: Keychain
|
||||||
init(service: KeychainControllerService,
|
/// The keychain responsible for storing all other secrets in the app (keyed by `Key`s).
|
||||||
accessGroup: String) {
|
private let mainKeychain: Keychain
|
||||||
keychain = Keychain(service: service.identifier,
|
|
||||||
accessGroup: accessGroup)
|
private enum Key: String {
|
||||||
|
case pinCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(service: KeychainControllerService, accessGroup: String) {
|
||||||
|
restorationTokenKeychain = Keychain(service: service.restorationTokenID, accessGroup: accessGroup)
|
||||||
|
mainKeychain = Keychain(service: service.mainID, accessGroup: accessGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Restoration Tokens
|
||||||
|
|
||||||
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
|
func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) {
|
||||||
do {
|
do {
|
||||||
let tokenData = try JSONEncoder().encode(restorationToken)
|
let tokenData = try JSONEncoder().encode(restorationToken)
|
||||||
try keychain.set(tokenData, key: username)
|
try restorationTokenKeychain.set(tokenData, key: username)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed storing user restore token with error: \(error)")
|
MXLog.error("Failed storing user restore token with error: \(error)")
|
||||||
}
|
}
|
||||||
@ -47,7 +59,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
|
|
||||||
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
|
func restorationTokenForUsername(_ username: String) -> RestorationToken? {
|
||||||
do {
|
do {
|
||||||
guard let tokenData = try keychain.getData(username) else {
|
guard let tokenData = try restorationTokenKeychain.getData(username) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +71,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func restorationTokens() -> [KeychainCredentials] {
|
func restorationTokens() -> [KeychainCredentials] {
|
||||||
keychain.allKeys().compactMap { username in
|
restorationTokenKeychain.allKeys().compactMap { username in
|
||||||
guard let restorationToken = restorationTokenForUsername(username) else {
|
guard let restorationToken = restorationTokenForUsername(username) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -72,7 +84,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
MXLog.warning("Removing restoration token for user: \(username).")
|
MXLog.warning("Removing restoration token for user: \(username).")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try keychain.remove(username)
|
try restorationTokenKeychain.remove(username)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed removing restore token with error: \(error)")
|
MXLog.error("Failed removing restore token with error: \(error)")
|
||||||
}
|
}
|
||||||
@ -82,7 +94,7 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
MXLog.warning("Removing all user restoration tokens.")
|
MXLog.warning("Removing all user restoration tokens.")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try keychain.removeAll()
|
try restorationTokenKeychain.removeAll()
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("Failed removing all tokens")
|
MXLog.error("Failed removing all tokens")
|
||||||
}
|
}
|
||||||
@ -103,4 +115,41 @@ class KeychainController: KeychainControllerProtocol {
|
|||||||
let restorationToken = RestorationToken(session: session)
|
let restorationToken = RestorationToken(session: session)
|
||||||
setRestorationToken(restorationToken, forUsername: session.userId)
|
setRestorationToken(restorationToken, forUsername: session.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - App Secrets
|
||||||
|
|
||||||
|
func resetSecrets() {
|
||||||
|
MXLog.warning("Resetting main keychain.")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try mainKeychain.removeAll()
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed resetting the main keychain.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsPINCode() throws -> Bool {
|
||||||
|
try mainKeychain.contains(Key.pinCode.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPINCode(_ pinCode: String) throws {
|
||||||
|
try mainKeychain.set(pinCode, key: Key.pinCode.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pinCode() -> String? {
|
||||||
|
do {
|
||||||
|
return try mainKeychain.getString(Key.pinCode.rawValue)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed retrieving the PIN code.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePINCode() {
|
||||||
|
do {
|
||||||
|
try mainKeychain.remove(Key.pinCode.rawValue)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed removing the PIN code.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,9 +24,24 @@ struct KeychainCredentials {
|
|||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol KeychainControllerProtocol: ClientSessionDelegate {
|
protocol KeychainControllerProtocol: ClientSessionDelegate {
|
||||||
|
// MARK: Restoration Tokens
|
||||||
|
|
||||||
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
|
func setRestorationToken(_ restorationToken: RestorationToken, forUsername: String)
|
||||||
func restorationTokenForUsername(_ username: String) -> RestorationToken?
|
func restorationTokenForUsername(_ username: String) -> RestorationToken?
|
||||||
func restorationTokens() -> [KeychainCredentials]
|
func restorationTokens() -> [KeychainCredentials]
|
||||||
func removeRestorationTokenForUsername(_ username: String)
|
func removeRestorationTokenForUsername(_ username: String)
|
||||||
func removeAllRestorationTokens()
|
func removeAllRestorationTokens()
|
||||||
|
|
||||||
|
// MARK: App Secrets
|
||||||
|
|
||||||
|
/// Removes everything from the keychain excluding any restoration tokens.
|
||||||
|
func resetSecrets()
|
||||||
|
/// Whether or not an App Lock PIN code has been set.
|
||||||
|
func containsPINCode() throws -> Bool
|
||||||
|
/// Sets a new PIN code for App Lock.
|
||||||
|
func setPINCode(_ pinCode: String) throws
|
||||||
|
/// The PIN code required to unlock the app.
|
||||||
|
func pinCode() -> String?
|
||||||
|
/// Removes the App Lock PIN code.
|
||||||
|
func removePINCode()
|
||||||
}
|
}
|
||||||
|
@ -21,19 +21,22 @@ import XCTest
|
|||||||
@MainActor
|
@MainActor
|
||||||
class AppLockScreenViewModelTests: XCTestCase {
|
class AppLockScreenViewModelTests: XCTestCase {
|
||||||
var appLockService: AppLockService!
|
var appLockService: AppLockService!
|
||||||
|
var keychainController: KeychainControllerMock!
|
||||||
var viewModel: AppLockScreenViewModelProtocol!
|
var viewModel: AppLockScreenViewModelProtocol!
|
||||||
|
|
||||||
var context: AppLockScreenViewModelType.Context { viewModel.context }
|
var context: AppLockScreenViewModelType.Context { viewModel.context }
|
||||||
|
|
||||||
override func setUp() {
|
override func setUp() {
|
||||||
AppSettings.reset()
|
AppSettings.reset()
|
||||||
appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: AppSettings())
|
keychainController = KeychainControllerMock()
|
||||||
|
appLockService = AppLockService(keychainController: keychainController, appSettings: AppSettings())
|
||||||
viewModel = AppLockScreenViewModel(appLockService: appLockService)
|
viewModel = AppLockScreenViewModel(appLockService: appLockService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUnlock() async throws {
|
func testUnlock() async throws {
|
||||||
// Given a valid PIN code.
|
// Given a valid PIN code.
|
||||||
let pinCode = "0000"
|
let pinCode = "2023"
|
||||||
|
keychainController.pinCodeReturnValue = pinCode
|
||||||
|
|
||||||
// When entering it on the lock screen.
|
// When entering it on the lock screen.
|
||||||
let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked }
|
let deferred = deferFulfillment(viewModel.actions) { $0 == .appUnlocked }
|
178
UnitTests/Sources/AppLock/AppLockServiceTests.swift
Normal file
178
UnitTests/Sources/AppLock/AppLockServiceTests.swift
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class AppLockServiceTests: XCTestCase {
|
||||||
|
var appSettings: AppSettings!
|
||||||
|
var service: AppLockService!
|
||||||
|
|
||||||
|
override func setUp() {
|
||||||
|
AppSettings.reset()
|
||||||
|
appSettings = AppSettings()
|
||||||
|
appSettings.appLockFlowEnabled = true
|
||||||
|
|
||||||
|
let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
|
|
||||||
|
service = AppLockService(keychainController: keychainController, appSettings: appSettings)
|
||||||
|
service.disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
AppSettings.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func testValidPINCode() {
|
||||||
|
// Given a service that hasn't been enabled.
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||||
|
|
||||||
|
// When setting a PIN code.
|
||||||
|
let pinCode = "2023" // Highly secure PIN that is rotated every 12 months.
|
||||||
|
guard case .success = service.setupPINCode(pinCode) else {
|
||||||
|
XCTFail("The PIN should be valid.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then service should be enabled and only the provided PIN should work to unlock the app.
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should become enabled when setting a PIN.")
|
||||||
|
XCTAssertTrue(service.unlock(with: pinCode), "The provided PIN code should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: "2024"), "No other PIN code should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: "1234"), "No other PIN code should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: "9999"), "No other PIN code should work.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWeakPINCode() {
|
||||||
|
// Given a service that hasn't been enabled.
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||||
|
|
||||||
|
// When setting a PIN code that is in the block list.
|
||||||
|
let pinCode = appSettings.appLockPINCodeBlockList[0]
|
||||||
|
let result = service.setupPINCode(pinCode)
|
||||||
|
|
||||||
|
// Then the setup should fail and the service be left as disabled.
|
||||||
|
guard case let .failure(error) = result else {
|
||||||
|
XCTFail("The call should have failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testShortPINCode() {
|
||||||
|
// Given a service that hasn't been enabled.
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||||
|
|
||||||
|
// When setting a PIN code that is too short
|
||||||
|
let pinCode = "123"
|
||||||
|
let result = service.setupPINCode(pinCode)
|
||||||
|
|
||||||
|
// Then the setup should fail and the service be left as disabled.
|
||||||
|
guard case let .failure(error) = result else {
|
||||||
|
XCTFail("The call should have failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNonNumericPINCode() {
|
||||||
|
// Given a service that hasn't been enabled.
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service shouldn't be enabled to begin with.")
|
||||||
|
|
||||||
|
// When setting a PIN code that is too short
|
||||||
|
let pinCode = "abcd"
|
||||||
|
let result = service.setupPINCode(pinCode)
|
||||||
|
|
||||||
|
// Then the setup should fail and the service be left as disabled.
|
||||||
|
guard case let .failure(error) = result else {
|
||||||
|
XCTFail("The call should have failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(error, .invalidPIN, "The PIN should be rejected as invalid.")
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service should remain disabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testChangePINCode() {
|
||||||
|
// Given a service that is already enabled with a PIN.
|
||||||
|
let pinCode = "2023"
|
||||||
|
let newPINCode = "2024"
|
||||||
|
guard case .success = service.setupPINCode(pinCode) else {
|
||||||
|
XCTFail("The PIN should be valid.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||||
|
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: newPINCode), "The PIN we're about to set should not work.")
|
||||||
|
|
||||||
|
// When updating the PIN code.
|
||||||
|
guard case .success = service.setupPINCode(newPINCode) else {
|
||||||
|
XCTFail("The PIN should be valid.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then the old code should not be accepted.
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||||
|
XCTAssertTrue(service.unlock(with: newPINCode), "The new PIN should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: pinCode), "The original PIN should be rejected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInvalidChangePINCode() {
|
||||||
|
// Given a service that is already enabled with a PIN.
|
||||||
|
let pinCode = "2023"
|
||||||
|
let invalidPIN = appSettings.appLockPINCodeBlockList[0]
|
||||||
|
guard case .success = service.setupPINCode(pinCode) else {
|
||||||
|
XCTFail("The PIN should be valid.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||||
|
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||||
|
XCTAssertFalse(service.unlock(with: invalidPIN), "The PIN we're about to set should not work.")
|
||||||
|
|
||||||
|
// When updating the PIN code that is in the block list.
|
||||||
|
let result = service.setupPINCode(invalidPIN)
|
||||||
|
|
||||||
|
// Then it should fail and nothing should change.
|
||||||
|
guard case let .failure(error) = result else {
|
||||||
|
XCTFail("The call should have failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(error, .weakPIN, "The PIN should be rejected as weak.")
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should remain enabled.")
|
||||||
|
XCTAssertFalse(service.unlock(with: invalidPIN), "The rejected PIN shouldn't work.")
|
||||||
|
XCTAssertTrue(service.unlock(with: pinCode), "The original PIN should continue to work.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDisablePINCode() {
|
||||||
|
// Given a service that is already enabled with a PIN.
|
||||||
|
let pinCode = "2023"
|
||||||
|
guard case .success = service.setupPINCode(pinCode) else {
|
||||||
|
XCTFail("The PIN should be valid.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(service.isEnabled, "The service should be enabled.")
|
||||||
|
XCTAssertTrue(service.unlock(with: pinCode), "The initial PIN should work.")
|
||||||
|
|
||||||
|
// When disabling the PIN code.
|
||||||
|
service.disable()
|
||||||
|
|
||||||
|
// Then the PIN code should be removed.
|
||||||
|
XCTAssertFalse(service.isEnabled, "The service should no longer be enabled.")
|
||||||
|
XCTAssertFalse(service.unlock(with: pinCode), "The initial PIN shouldn't work any more.")
|
||||||
|
}
|
||||||
|
}
|
154
UnitTests/Sources/AppLock/AppLockTimerTests.swift
Normal file
154
UnitTests/Sources/AppLock/AppLockTimerTests.swift
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
//
|
||||||
|
// 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 AppLockTimerTests: XCTestCase {
|
||||||
|
var timer: AppLockTimer!
|
||||||
|
|
||||||
|
let now = Date.now
|
||||||
|
|
||||||
|
var gracePeriod: TimeInterval { timer.gracePeriod }
|
||||||
|
var halfGracePeriod: TimeInterval { gracePeriod / 2 }
|
||||||
|
var gracePeriodX2: TimeInterval { gracePeriod * 2 }
|
||||||
|
var gracePeriodX10: TimeInterval { gracePeriod * 10 }
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimerLockedOnStartup() {
|
||||||
|
setupTimer(unlocked: false)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now),
|
||||||
|
"The app should be locked on a fresh launch.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
||||||
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
||||||
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
||||||
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
||||||
|
"The app should be locked after a fresh launch.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimerBeforeFirstUnlock() {
|
||||||
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now),
|
||||||
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
||||||
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
||||||
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
||||||
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
||||||
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimerWhenUnlocked() {
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: now + 1),
|
||||||
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
||||||
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
||||||
|
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||||
|
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
||||||
|
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimerRepeatingWithinGracePeriod() {
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
|
||||||
|
var nextCheck = now + halfGracePeriod
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
||||||
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
|
nextCheck = now + gracePeriod
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
||||||
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
|
nextCheck = now + gracePeriod + halfGracePeriod
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
||||||
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
|
nextCheck = now + gracePeriodX2
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
||||||
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
|
nextCheck = now + gracePeriodX10
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
||||||
|
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTimerWithLongForeground() {
|
||||||
|
setupTimer(unlocked: true)
|
||||||
|
|
||||||
|
let backgroundDate = now + gracePeriodX10
|
||||||
|
timer.applicationDidEnterBackground(date: backgroundDate)
|
||||||
|
|
||||||
|
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: backgroundDate + 1),
|
||||||
|
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testChangingTimeLocksApp() {
|
||||||
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now - 1),
|
||||||
|
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up the timer for testing.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - unlocked: Whether the timer should consider itself unlocked or not.
|
||||||
|
/// - backgroundedDate: If not nil, the timer will consider the app to have been backgrounded at the specified date.
|
||||||
|
private func setupTimer(unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
||||||
|
timer = AppLockTimer(gracePeriod: 180)
|
||||||
|
if unlocked {
|
||||||
|
timer.registerUnlock()
|
||||||
|
}
|
||||||
|
if let backgroundedDate {
|
||||||
|
timer.applicationDidEnterBackground(date: backgroundedDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ class KeychainControllerTests: XCTestCase {
|
|||||||
keychain = KeychainController(service: .tests,
|
keychain = KeychainController(service: .tests,
|
||||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
keychain.removeAllRestorationTokens()
|
keychain.removeAllRestorationTokens()
|
||||||
|
keychain.resetSecrets()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testAddRestorationToken() {
|
func testAddRestorationToken() {
|
||||||
@ -113,4 +114,45 @@ class KeychainControllerTests: XCTestCase {
|
|||||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test3:example.com"), "The restoration token should not have been deleted.")
|
XCTAssertNotNil(keychain.restorationTokenForUsername("@test3:example.com"), "The restoration token should not have been deleted.")
|
||||||
XCTAssertNotNil(keychain.restorationTokenForUsername("@test4:example.com"), "The restoration token should not have been deleted.")
|
XCTAssertNotNil(keychain.restorationTokenForUsername("@test4:example.com"), "The restoration token should not have been deleted.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAddPINCode() throws {
|
||||||
|
// Given a keychain without a PIN code set.
|
||||||
|
try XCTAssertFalse(keychain.containsPINCode(), "A new keychain shouldn't contain a PIN code.")
|
||||||
|
XCTAssertNil(keychain.pinCode(), "A new keychain shouldn't return a PIN code.")
|
||||||
|
|
||||||
|
// When setting a PIN code.
|
||||||
|
try keychain.setPINCode("0000")
|
||||||
|
|
||||||
|
// The the PIN code should be stored.
|
||||||
|
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||||
|
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdatePINCode() throws {
|
||||||
|
// Given a keychain with a PIN code already set.
|
||||||
|
try keychain.setPINCode("0000")
|
||||||
|
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||||
|
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||||
|
|
||||||
|
// When setting a different PIN code.
|
||||||
|
try keychain.setPINCode("1234")
|
||||||
|
|
||||||
|
// The the PIN code should be updated.
|
||||||
|
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should still contain the PIN code.")
|
||||||
|
XCTAssertEqual(keychain.pinCode(), "1234", "The stored PIN code should match the new value.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRemovePINCode() throws {
|
||||||
|
// Given a keychain with a PIN code already set.
|
||||||
|
try keychain.setPINCode("0000")
|
||||||
|
try XCTAssertTrue(keychain.containsPINCode(), "The keychain should contain the PIN code.")
|
||||||
|
XCTAssertEqual(keychain.pinCode(), "0000", "The stored PIN code should match what was set.")
|
||||||
|
|
||||||
|
// When removing the PIN code.
|
||||||
|
keychain.removePINCode()
|
||||||
|
|
||||||
|
// The the PIN code should no longer be stored.
|
||||||
|
try XCTAssertFalse(keychain.containsPINCode(), "The keychain should no longer contain the PIN code.")
|
||||||
|
XCTAssertNil(keychain.pinCode(), "There shouldn't be a stored PIN code after removing it.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
changelog.d/pr-1912.wip
Normal file
1
changelog.d/pr-1912.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
Initial service implementation for using a PIN code
|
Loading…
x
Reference in New Issue
Block a user