diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 46879c2e7..74972a60e 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 21C7DEFC26669A3700C44800 /* CalendarExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */; }; 21C7DEFE26669CE100C44800 /* DateFormattingExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56BD3723438C7000A7371A /* DateFormattingExtensions.swift */; }; 21C7DF09266C0D8F00C44800 /* EnterpriseServerApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */; }; - 21C7DF0B266C0E3600C44800 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21C7DF0A266C0E3600C44800 /* Configuration.swift */; }; 21CE25E62650070300ADFF4B /* WkdUrlConstructor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CE25E52650070300ADFF4B /* WkdUrlConstructor.swift */; }; 21EA3B592656611D00691848 /* ClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21EA3B15265647C400691848 /* ClientConfiguration.swift */; }; 21F836B62652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F836B52652A26B00B2448C /* DataExntensions+ZBase32Encoding.swift */; }; @@ -38,6 +37,8 @@ 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */; }; 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */; }; 2C60AB0C272564D40040D7F2 /* InvalidStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */; }; + 2CAF25322756C37E005C7C7C /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF25312756C37E005C7C7C /* AppContext.swift */; }; + 2CAF25342756C3A6005C7C7C /* ImapSessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF25332756C3A6005C7C7C /* ImapSessionProvider.swift */; }; 2CC12C3F273571B80021DDDF /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC12C3E273571B80021DDDF /* AttachmentManager.swift */; }; 2CC50FAF27440B2C0051629A /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC50FAE27440B2C0051629A /* Session.swift */; }; 2CC50FB12744167A0051629A /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC50FB02744167A0051629A /* Folder.swift */; }; @@ -108,7 +109,7 @@ 7F72537A0C44D3CE670F0EFD /* Pods_FlowCryptUIApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3382C015A576728FA08BA310 /* Pods_FlowCryptUIApplication.framework */; }; 949ED9422303E3B400530579 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 949ED9412303E3B400530579 /* Colors.xcassets */; }; 9F003D6125E1B4ED00EB38C0 /* TrashFolderProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D6025E1B4ED00EB38C0 /* TrashFolderProvider.swift */; }; - 9F003D6D25EA8F3200EB38C0 /* UserAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D6C25EA8F3200EB38C0 /* UserAccountService.swift */; }; + 9F003D6D25EA8F3200EB38C0 /* SessionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003D6C25EA8F3200EB38C0 /* SessionService.swift */; }; 9F003DB625EA92BC00EB38C0 /* LogOutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F003DB525EA92BC00EB38C0 /* LogOutHandler.swift */; }; 9F0C3C102316DD5B00299985 /* GoogleUserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C0F2316DD5B00299985 /* GoogleUserService.swift */; }; 9F0C3C122316DDA500299985 /* DataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0C3C112316DDA500299985 /* DataService.swift */; }; @@ -222,14 +223,12 @@ 9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0157926565B7800CBBA05 /* KeyMethodsTest.swift */; }; 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */; }; 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBCF266EBE1D00F3BF5D /* PassPhraseStorageMock.swift */; }; - 9FC41183268118B1004C0A69 /* EmailProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */; }; 9FC413182683C492004C0A69 /* InMemoryPassPhraseStorageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC413172683C491004C0A69 /* InMemoryPassPhraseStorageTest.swift */; }; 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC413432683C912004C0A69 /* GmailServiceTest.swift */; }; 9FC7EB76266EB67B00F3BF5D /* EncryptedStorageProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */; }; 9FC7EBAA266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */; }; 9FD364862381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */; }; 9FDF3654235A218E00614596 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF3653235A218E00614596 /* main.swift */; }; - 9FDF3656235A22DA00614596 /* AppReset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDF3655235A22DA00614596 /* AppReset.swift */; }; 9FE1B3802563F85400D6D086 /* MessagesListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B37F2563F85400D6D086 /* MessagesListProvider.swift */; }; 9FE1B3942563F98600D6D086 /* Imap+MessagesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B3932563F98600D6D086 /* Imap+MessagesList.swift */; }; 9FE1B3A02565B0CE00D6D086 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE1B39F2565B0CD00D6D086 /* Message.swift */; }; @@ -434,7 +433,6 @@ 2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; 21C7DEFB26669A3700C44800 /* CalendarExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExtension.swift; sourceTree = ""; }; 21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterpriseServerApi.swift; sourceTree = ""; }; - 21C7DF0A266C0E3600C44800 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; 21CE25E52650070300ADFF4B /* WkdUrlConstructor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WkdUrlConstructor.swift; sourceTree = ""; }; 21EA3B15265647C400691848 /* ClientConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientConfiguration.swift; sourceTree = ""; }; 21EA3B2226565B5D00691848 /* RawClientConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawClientConfigurationTests.swift; sourceTree = ""; }; @@ -451,6 +449,8 @@ 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceTests.swift; sourceTree = ""; }; 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseServiceMock.swift; sourceTree = ""; }; 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidStorageViewController.swift; sourceTree = ""; }; + 2CAF25312756C37E005C7C7C /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; + 2CAF25332756C3A6005C7C7C /* ImapSessionProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImapSessionProvider.swift; sourceTree = ""; }; 2CC12C3E273571B80021DDDF /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = ""; }; 2CC50FAE27440B2C0051629A /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 2CC50FB02744167A0051629A /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; @@ -526,7 +526,7 @@ 601EEE30272B19D200FE445B /* CheckMailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckMailAuthViewController.swift; sourceTree = ""; }; 949ED9412303E3B400530579 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 9F003D6025E1B4ED00EB38C0 /* TrashFolderProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashFolderProvider.swift; sourceTree = ""; }; - 9F003D6C25EA8F3200EB38C0 /* UserAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccountService.swift; sourceTree = ""; }; + 9F003D6C25EA8F3200EB38C0 /* SessionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionService.swift; sourceTree = ""; }; 9F003D9D25EA910B00EB38C0 /* LocalStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStorageTests.swift; sourceTree = ""; }; 9F003DB525EA92BC00EB38C0 /* LogOutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogOutHandler.swift; sourceTree = ""; }; 9F0C3C0F2316DD5B00299985 /* GoogleUserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleUserService.swift; sourceTree = ""; }; @@ -660,7 +660,6 @@ 9FC7EB75266EB67B00F3BF5D /* EncryptedStorageProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorageProtocols.swift; sourceTree = ""; }; 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseStorageTests.swift; sourceTree = ""; }; 9FC7EBA9266EBD3700F3BF5D /* InMemoryPassPhraseStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InMemoryPassPhraseStorage.swift; sourceTree = ""; }; - 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailProviderMock.swift; sourceTree = ""; }; 9FC7EBCF266EBE1D00F3BF5D /* PassPhraseStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseStorageMock.swift; sourceTree = ""; }; 9FD22A19230FD781005067A6 /* NavigationBarItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarItemsView.swift; sourceTree = ""; }; 9FD22A1B230FE7D0005067A6 /* Then.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Then.swift; sourceTree = ""; }; @@ -668,7 +667,6 @@ 9FD364852381EFCB00657302 /* SetupManuallyEnterPassPhraseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupManuallyEnterPassPhraseViewController.swift; sourceTree = ""; }; 9FDF3637235A0B3100614596 /* InfoCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCellNode.swift; sourceTree = ""; }; 9FDF3653235A218E00614596 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - 9FDF3655235A22DA00614596 /* AppReset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReset.swift; sourceTree = ""; }; 9FE1B37F2563F85400D6D086 /* MessagesListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesListProvider.swift; sourceTree = ""; }; 9FE1B3932563F98600D6D086 /* Imap+MessagesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Imap+MessagesList.swift"; sourceTree = ""; }; 9FE1B39F2565B0CD00D6D086 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; @@ -1189,14 +1187,15 @@ isa = PBXGroup; children = ( 32DCA377D22F4D67A8FA05EB /* Imap.swift */, - 9F31AB90232993F500CF87EA /* Imap+session.swift */, - 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */, - 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */, 9F56BD2B23438A8500A7371A /* Imap+messages.swift */, - 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */, - 9F0C3C19231819C500299985 /* MessageKindProviderType.swift */, + 32DCA55C094E9745AA1FD210 /* Imap+msg.swift */, 9F53CB862555E7F300C0157A /* Imap+Other.swift */, + 9F31AB8B23298B3F00CF87EA /* Imap+retry.swift */, + 9F31AB90232993F500CF87EA /* Imap+session.swift */, 9F5C2A8A257E6C4900DE9B4B /* ImapError.swift */, + 9F3EF32423B15C1400FA0CEF /* ImapHelper.swift */, + 2CAF25332756C3A6005C7C7C /* ImapSessionProvider.swift */, + 9F0C3C19231819C500299985 /* MessageKindProviderType.swift */, ); path = Imap; sourceTree = ""; @@ -1405,9 +1404,8 @@ 9F8220D32633661C004B2009 /* App */ = { isa = PBXGroup; children = ( + 2CAF25312756C37E005C7C7C /* AppContext.swift */, 9FDF3653235A218E00614596 /* main.swift */, - 9FDF3655235A22DA00614596 /* AppReset.swift */, - 21C7DF0A266C0E3600C44800 /* Configuration.swift */, ); path = App; sourceTree = ""; @@ -1516,7 +1514,6 @@ isa = PBXGroup; children = ( 9FC7EBA2266EB95300F3BF5D /* PassPhraseStorageTests.swift */, - 9FC7EBC1266EBE0100F3BF5D /* EmailProviderMock.swift */, 9FC7EBCF266EBE1D00F3BF5D /* PassPhraseStorageMock.swift */, 9FC413172683C491004C0A69 /* InMemoryPassPhraseStorageTest.swift */, ); @@ -1939,7 +1936,7 @@ 9F0C3C112316DDA500299985 /* DataService.swift */, 9F589F1A238C85B0007FD759 /* Encrypted Storage */, 9F589F19238C82A1007FD759 /* Local Storage */, - 9F003D6C25EA8F3200EB38C0 /* UserAccountService.swift */, + 9F003D6C25EA8F3200EB38C0 /* SessionService.swift */, 9F003DB525EA92BC00EB38C0 /* LogOutHandler.swift */, 51775C38270C7D2400D7C944 /* StorageMethod.swift */, ); @@ -2485,7 +2482,13 @@ 9F976584267E194F0058419D /* TestData.swift in Sources */, 9F6F3C6A26ADFBEB005BD9C6 /* MessageGatewayMock.swift in Sources */, 9F7E903926A1AD7A0021C07F /* KeyDetailsTests.swift in Sources */, - 9FC41183268118B1004C0A69 /* EmailProviderMock.swift in Sources */, + 51B0C774272AB61000124663 /* StringTestExtension.swift in Sources */, + 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */, + 9F976490267E11880058419D /* ImapHelperTest.swift in Sources */, + 9F5F501D26F90AE100294FA2 /* OrganisationalRulesServiceMock.swift in Sources */, + 9F976584267E194F0058419D /* TestData.swift in Sources */, + 9F6F3C6A26ADFBEB005BD9C6 /* MessageGatewayMock.swift in Sources */, + 9F7E903926A1AD7A0021C07F /* KeyDetailsTests.swift in Sources */, 51B0C774272AB61000124663 /* StringTestExtension.swift in Sources */, 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */, 9F976490267E11880058419D /* ImapHelperTest.swift in Sources */, @@ -2495,6 +2498,8 @@ 9FC413182683C492004C0A69 /* InMemoryPassPhraseStorageTest.swift in Sources */, 9F9764C5267E14AB0058419D /* GeneralConstantsTest.swift in Sources */, 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */, + 9F9764C5267E14AB0058419D /* GeneralConstantsTest.swift in Sources */, + 9F976507267E165D0058419D /* ZBase32EncodingTests.swift in Sources */, 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */, 9F5F504A26FA6C8F00294FA2 /* ClientConfigurationProviderMock.swift in Sources */, 9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */, @@ -2508,9 +2513,19 @@ 9F9764F4267E15CC0058419D /* ExtensionTests.swift in Sources */, 9F5F503526F90E5F00294FA2 /* ClientConfigurationServiceTests.swift in Sources */, 9F2F206826AEEAA60044E144 /* CombineTestExtension.swift in Sources */, + 9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */, + 9F6F3C7D26ADFC60005BD9C6 /* ContactsServiceMock.swift in Sources */, + 9F6F3C3C26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift in Sources */, + 9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */, + 9F97653D267E17C90058419D /* LocalStorageTests.swift in Sources */, + A34D222A27294C67004E0220 /* PubLookupTest.swift in Sources */, + 9F9764F4267E15CC0058419D /* ExtensionTests.swift in Sources */, + 9F5F503526F90E5F00294FA2 /* ClientConfigurationServiceTests.swift in Sources */, + 9F2F206826AEEAA60044E144 /* CombineTestExtension.swift in Sources */, 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */, 9F9500AF26F4BAE300E8C78B /* ClientConfigurationTests.swift in Sources */, 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */, + 9FC413442683C912004C0A69 /* GmailServiceTest.swift in Sources */, 9F976556267E186D0058419D /* RawClientConfigurationTests.swift in Sources */, A36108E9273C7A2E00A90E34 /* MockError.swift in Sources */, 9F7E8EC6269877E70021C07F /* KeyInfoTests.swift in Sources */, @@ -2518,6 +2533,9 @@ 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */, + 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, + 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, + 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2676,9 +2694,8 @@ 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */, D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */, D2CDC3D22402D4DA002B045F /* UIViewControllerExtensions.swift in Sources */, - 21C7DF0B266C0E3600C44800 /* Configuration.swift in Sources */, D2D27B79248A8694007346FA /* BigIntExtension.swift in Sources */, - 9F003D6D25EA8F3200EB38C0 /* UserAccountService.swift in Sources */, + 9F003D6D25EA8F3200EB38C0 /* SessionService.swift in Sources */, 5180CB9527357BB0001FC7EF /* WkdApi.swift in Sources */, D29AFFF92409767F00C1387D /* GoogleContactsResponse.swift in Sources */, 9F003D6125E1B4ED00EB38C0 /* TrashFolderProvider.swift in Sources */, @@ -2708,7 +2725,6 @@ D20D3C6E2520AB3900D4AA9A /* BackupViewDecorator.swift in Sources */, 9F268891237DC55600428A94 /* SetupManuallyImportKeyViewController.swift in Sources */, D2E26F7024F266F300612AF1 /* ContactDetailViewController.swift in Sources */, - 9FDF3656235A22DA00614596 /* AppReset.swift in Sources */, 5A39F43F239EE7D2001F4607 /* SegmentedViewController.swift in Sources */, 21489B7A267CB4DF00BDE4AC /* ClientConfigurationRealmObject.swift in Sources */, 5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */, @@ -2730,6 +2746,8 @@ 9F17976D2368EEBD002BF770 /* SetupViewDecorator.swift in Sources */, 5ADEDCC023A43B0800EC495E /* KeyDetailInfoViewDecorator.swift in Sources */, D227C0E6250538780070F805 /* RemoteFoldersProvider.swift in Sources */, + 2CAF25322756C37E005C7C7C /* AppContext.swift in Sources */, + 2CAF25342756C3A6005C7C7C /* ImapSessionProvider.swift in Sources */, 9F2AC5B1267BDED100F6149B /* GmailSearchExpressionGenerator.swift in Sources */, 9F953E09238310D500AEB98B /* KeyMethods.swift in Sources */, 9F92EE72236F165E009BE0D7 /* EncryptedStorage.swift in Sources */, diff --git a/FlowCrypt/App/AppContext.swift b/FlowCrypt/App/AppContext.swift new file mode 100644 index 000000000..a30f8310e --- /dev/null +++ b/FlowCrypt/App/AppContext.swift @@ -0,0 +1,133 @@ +// +// AppContext.swift +// FlowCrypt +// +// Created by Tom on 30.11.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation +import UIKit + +class AppContext { + + let globalRouter: GlobalRouterType + let encryptedStorage: EncryptedStorageType + let session: SessionType? + // todo - session service should have maybe `.currentSession` on it, then we don't have to have `session` above? + let userAccountService: SessionServiceType + let dataService: DataServiceType + let keyStorage: KeyStorageType + let keyService: KeyServiceType + let passPhraseService: PassPhraseServiceType + let clientConfigurationService: ClientConfigurationServiceType + + private init( + encryptedStorage: EncryptedStorageType, + session: SessionType?, + userAccountService: SessionServiceType, + dataService: DataServiceType, + keyStorage: KeyStorageType, + keyService: KeyServiceType, + passPhraseService: PassPhraseServiceType, + clientConfigurationService: ClientConfigurationServiceType, + globalRouter: GlobalRouterType + ) { + self.encryptedStorage = encryptedStorage + self.session = session + self.userAccountService = userAccountService + self.dataService = dataService + self.keyStorage = keyStorage // todo - keyStorage and keyService should be the same + self.keyService = keyService + self.passPhraseService = passPhraseService + self.clientConfigurationService = clientConfigurationService + self.globalRouter = globalRouter + } + + @MainActor + static func setUpAppContext(globalRouter: GlobalRouterType) throws -> AppContext { + let keyChainService = KeyChainService() + let encryptedStorage = EncryptedStorage( + storageEncryptionKey: try keyChainService.getStorageEncryptionKey() + ) + let dataService = DataService(encryptedStorage: encryptedStorage) + let passPhraseService = PassPhraseService(encryptedStorage: encryptedStorage) + let keyStorage = KeyDataStorage(encryptedStorage: encryptedStorage) + let keyService = KeyService( + storage: keyStorage, + passPhraseService: passPhraseService, + currentUserEmail: { dataService.email } + ) + let clientConfigurationService = ClientConfigurationService( + local: LocalClientConfiguration( + encryptedStorage: encryptedStorage + ) + ) + return AppContext( + encryptedStorage: encryptedStorage, + session: nil, // will be set later. But would be nice to already set here, if available + userAccountService: SessionService( + encryptedStorage: encryptedStorage, + dataService: dataService, + googleService: GoogleUserService( + currentUserEmail: dataService.currentUser?.email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate + ) + ), + dataService: dataService, + keyStorage: keyStorage, + keyService: keyService, + passPhraseService: passPhraseService, + clientConfigurationService: clientConfigurationService, + globalRouter: globalRouter + ) + } + + func withSession(_ session: SessionType?) -> AppContext { + return AppContext( + encryptedStorage: self.encryptedStorage, + session: session, + userAccountService: self.userAccountService, + dataService: self.dataService, + keyStorage: self.keyStorage, + keyService: self.keyService, + passPhraseService: self.passPhraseService, + clientConfigurationService: self.clientConfigurationService, + globalRouter: globalRouter + ) + } + + func getRequiredMailProvider() -> MailProvider { + guard let mailProvider = getOptionalMailProvider() else { + // todo - should throw instead + fatalError("wrongly using mail provider when not logged in") + } + return mailProvider + } + + func getOptionalMailProvider() -> MailProvider? { + guard let currentUser = self.dataService.currentUser, let currentAuthType = self.dataService.currentAuthType else { + return nil + } + return MailProvider( + currentAuthType: currentAuthType, + currentUser: currentUser + ) + } + + func getBackupService() -> BackupService { + let mailProvider = self.getRequiredMailProvider() + return BackupService( + backupProvider: mailProvider.backupProvider, + messageSender: mailProvider.messageSender + ) + } + + func getFoldersService() -> FoldersService { + return FoldersService( + encryptedStorage: self.encryptedStorage, + remoteFoldersProvider: self.getRequiredMailProvider().remoteFoldersProvider + ) + } + +} diff --git a/FlowCrypt/App/AppReset.swift b/FlowCrypt/App/AppReset.swift deleted file mode 100644 index dce00ed93..000000000 --- a/FlowCrypt/App/AppReset.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// AppReset.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 18.10.2019. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -enum AppReset: String { - case reset = "--reset" - - static func resetKeychain() { - let secClasses = [ - kSecClassGenericPassword as String, - kSecClassInternetPassword as String, - kSecClassCertificate as String, - kSecClassKey as String, - kSecClassIdentity as String - ] - for secClass in secClasses { - let query = [kSecClass as String: secClass] - SecItemDelete(query as CFDictionary) - } - } - - static func resetUserDefaults() { - guard let identifier = Bundle.main.bundleIdentifier else { return } - UserDefaults.standard.removePersistentDomain(forName: identifier) - } -} diff --git a/FlowCrypt/App/Configuration.swift b/FlowCrypt/App/Configuration.swift deleted file mode 100644 index 454ee7676..000000000 --- a/FlowCrypt/App/Configuration.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Configuration.swift -// FlowCrypt -// -// Created by Yevhen Kyivskyi on 05.06.2021. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation - -class Configuration { - static let publicEmailProviderDomains = ["gmail.com", "googlemail.com", "outlook.com"] -} diff --git a/FlowCrypt/App/main.swift b/FlowCrypt/App/main.swift index 3a711452a..02103f42e 100644 --- a/FlowCrypt/App/main.swift +++ b/FlowCrypt/App/main.swift @@ -9,16 +9,9 @@ import Foundation import UIKit -autoreleasepool { - if ProcessInfo().arguments.contains(AppReset.reset.rawValue) { - AppReset.resetKeychain() - AppReset.resetUserDefaults() - } - - UIApplicationMain( - CommandLine.argc, - CommandLine.unsafeArgv, - nil, - NSStringFromClass(AppDelegate.self) - ) -} +UIApplicationMain( + CommandLine.argc, + CommandLine.unsafeArgv, + nil, + NSStringFromClass(AppDelegate.self) +) diff --git a/FlowCrypt/AppDelegate.swift b/FlowCrypt/AppDelegate.swift index f0c3af76a..048e5e046 100644 --- a/FlowCrypt/AppDelegate.swift +++ b/FlowCrypt/AppDelegate.swift @@ -5,15 +5,15 @@ import AppAuth import UIKit +import GTMAppAuth -class AppDelegate: UIResponder, UIApplicationDelegate { +class AppDelegate: UIResponder, UIApplicationDelegate, AppDelegateGoogleSesssionContainer { var blurViewController: BlurViewController? var googleAuthSession: OIDExternalUserAgentSession? let window: UIWindow = UIWindow(frame: UIScreen.main.bounds) - func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - let isRunningTests = NSClassFromString("XCTestCase") != nil - if isRunningTests { + func application(_ application: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + if application.isRunningTests { return true } GlobalRouter().proceed() diff --git a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift index e18429f98..ce99ad63d 100644 --- a/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift +++ b/FlowCrypt/Controllers/Bootstrap/InvalidStorageViewController.swift @@ -18,10 +18,10 @@ final class InvalidStorageViewController: TableNodeViewController { } private let error: Error - private let encryptedStorage: EncryptedStorageType + private let encryptedStorage: EncryptedStorageType? // nil if failed to initialize private let router: GlobalRouterType - init(error: Error, encryptedStorage: EncryptedStorageType, router: GlobalRouterType) { + init(error: Error, encryptedStorage: EncryptedStorageType?, router: GlobalRouterType) { self.error = error self.encryptedStorage = encryptedStorage self.router = router @@ -44,6 +44,10 @@ final class InvalidStorageViewController: TableNodeViewController { } @objc private func handleTap() { + guard let encryptedStorage = encryptedStorage else { + showAlert(message: "invalid_storage_failed_to_initialize".localized) + return + } do { try encryptedStorage.reset() router.proceed() diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index 15bd95819..3439f317b 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -12,10 +12,10 @@ import FlowCryptUI import UIKit class CheckMailAuthViewController: TableNodeViewController { - private let globalRouter: GlobalRouterType + private let appContext: AppContext - init(globalRouter: GlobalRouterType = GlobalRouter()) { - self.globalRouter = globalRouter + init(appContext: AppContext) { + self.appContext = appContext super.init(node: TableNode()) } @@ -85,7 +85,7 @@ extension CheckMailAuthViewController { case 2: return ButtonCellNode(input: .signInAgain) { [weak self] in guard let self = self else { return } - self.globalRouter.signIn(with: .gmailLogin(self)) + self.appContext.globalRouter.signIn(appContext: self.appContext, route: .gmailLogin(self)) } default: return ASCellNode() diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 91e01df4a..97adde523 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -39,6 +39,7 @@ final class ComposeViewController: TableNodeViewController { case subject, subjectDivider, text } + private let appContext: AppContext private let composeMessageService: ComposeMessageService private let notificationCenter: NotificationCenter private let decorator: ComposeViewDecorator @@ -46,11 +47,10 @@ final class ComposeViewController: TableNodeViewController { private let cloudContactProvider: CloudContactsProvider private let filesManager: FilesManagerType private let photosManager: PhotosManagerType - private let keyService: KeyServiceType private let keyMethods: KeyMethodsType private let service: ServiceActor - private let passPhraseService: PassPhraseService private let router: GlobalRouterType + private let clientConfiguration: ClientConfiguration private let search = PassthroughSubject() private let userDefaults: UserDefaults @@ -68,42 +68,59 @@ final class ComposeViewController: TableNodeViewController { private var composedLatestDraft: ComposedDraft? init( - email: String, + appContext: AppContext, notificationCenter: NotificationCenter = .default, decorator: ComposeViewDecorator = ComposeViewDecorator(), input: ComposeMessageInput = .empty, - cloudContactProvider: CloudContactsProvider = UserContactsProvider(), + cloudContactProvider: CloudContactsProvider? = nil, userDefaults: UserDefaults = .standard, - contactsService: ContactsServiceType = ContactsService(), - composeMessageService: ComposeMessageService = ComposeMessageService(), + contactsService: ContactsServiceType? = nil, + composeMessageService: ComposeMessageService? = nil, filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), - keyService: KeyServiceType = KeyService(), - passPhraseService: PassPhraseService = PassPhraseService(), keyMethods: KeyMethodsType = KeyMethods(), router: GlobalRouterType = GlobalRouter() ) { + self.appContext = appContext + guard let email = appContext.dataService.email else { + fatalError("missing current user email") // todo - need a more elegant solution + } self.email = email self.notificationCenter = notificationCenter self.input = input self.decorator = decorator self.userDefaults = userDefaults - self.contactsService = contactsService + let clientConfiguration = appContext.clientConfigurationService.getSaved(for: email) + self.contactsService = contactsService ?? ContactsService( + localContactsProvider: LocalContactsProvider( + encryptedStorage: appContext.encryptedStorage + ), + clientConfiguration: clientConfiguration + ) + let cloudContactProvider = cloudContactProvider ?? UserContactsProvider( + userService: GoogleUserService( + currentUserEmail: email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate + ) + ) self.cloudContactProvider = cloudContactProvider - self.composeMessageService = composeMessageService + self.composeMessageService = composeMessageService ?? ComposeMessageService( + clientConfiguration: clientConfiguration, + encryptedStorage: appContext.encryptedStorage, + messageGateway: appContext.getRequiredMailProvider().messageSender + ) self.filesManager = filesManager self.photosManager = photosManager - self.keyService = keyService self.keyMethods = keyMethods self.service = ServiceActor( - composeMessageService: composeMessageService, - contactsService: contactsService, + composeMessageService: self.composeMessageService, + contactsService: self.contactsService, cloudContactProvider: cloudContactProvider ) - self.passPhraseService = passPhraseService self.router = router self.contextToSend.subject = input.subject self.contextToSend.attachments = input.attachments + self.clientConfiguration = clientConfiguration super.init(node: TableNode()) } @@ -354,7 +371,7 @@ extension ComposeViewController { extension ComposeViewController { private func prepareSigningKey() async throws -> PrvKeyInfo { - guard let signingKey = try await keyService.getSigningKey() else { + guard let signingKey = try await appContext.keyService.getSigningKey() else { throw AppErr.general("None of your private keys have your user id \"\(email)\". Please import the appropriate key.") } @@ -396,7 +413,7 @@ extension ComposeViewController { private func handlePassPhraseEntry(_ passPhrase: String, for signingKey: PrvKeyInfo) async throws -> Bool { // since pass phrase was entered (an inconvenient thing for user to do), // let's find all keys that match and save the pass phrase for all - let allKeys = try await self.keyService.getPrvKeyInfo() + let allKeys = try await appContext.keyService.getPrvKeyInfo() guard allKeys.isNotEmpty else { // tom - todo - nonsensical error type choice https://github.com/FlowCrypt/flowcrypt-ios/issues/859 // I copied it from another usage, but has to be changed @@ -404,7 +421,7 @@ extension ComposeViewController { } let matchingKeys = try await self.keyMethods.filterByPassPhraseMatch(keys: allKeys, passPhrase: passPhrase) // save passphrase for all matching keys - self.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) + appContext.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) // now figure out if the pass phrase also matched the signing prv itself let matched = matchingKeys.first(where: { $0.fingerprints.first == signingKey.fingerprints.first }) return matched != nil// true if the pass phrase matched signing key @@ -1110,7 +1127,7 @@ extension ComposeViewController { Task { do { - try await router.askForContactsPermission(for: .gmailLogin(self)) + try await router.askForContactsPermission(for: .gmailLogin(self), appContext: appContext) node.reloadSections([2], with: .automatic) } catch { handleContactsPermissionError(error) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index c72361073..6c7465e8e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -9,7 +9,6 @@ import FlowCryptUI import UIKit -typealias RecipientState = RecipientEmailsCellNode.Input.State typealias RecipientStateContext = RecipientEmailsCellNode.Input.StateContext struct ComposeViewDecorator { diff --git a/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift b/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift index 9e7c26a6f..7e3358f3d 100644 --- a/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift +++ b/FlowCrypt/Controllers/Inbox/Container/InboxViewContainerController.swift @@ -29,18 +29,26 @@ final class InboxViewContainerController: TableNodeViewController { case loadedFolders([FolderViewModel]) } - let folderService: FoldersServiceType + let appContext: AppContext + let foldersService: FoldersServiceType let decorator: InboxViewControllerContainerDecorator + let currentUser: User private var state: State = .loading { didSet { handleNewState() } } init( - folderService: FoldersServiceType = FoldersService(), + appContext: AppContext, + foldersService: FoldersServiceType? = nil, decorator: InboxViewControllerContainerDecorator = InboxViewControllerContainerDecorator() ) { - self.folderService = folderService + guard let currentUser = appContext.dataService.currentUser else { + fatalError("missing current user") // todo - use DI + } + self.currentUser = currentUser + self.appContext = appContext + self.foldersService = foldersService ?? appContext.getFoldersService() self.decorator = decorator super.init(node: TableNode()) node.delegate = self @@ -60,7 +68,7 @@ final class InboxViewContainerController: TableNodeViewController { private func fetchInboxFolder() { Task { do { - let folders = try await folderService.fetchFolders(isForceReload: true) + let folders = try await foldersService.fetchFolders(isForceReload: true, for: self.currentUser) self.handleFetched(folders: folders) } catch { self.state = .error(error) @@ -99,7 +107,7 @@ final class InboxViewContainerController: TableNodeViewController { return } let input = InboxViewModel(inbox) - let inboxViewController = InboxViewControllerFactory.make(with: input) + let inboxViewController = InboxViewControllerFactory.make(appContext: appContext, with: input) navigationController?.setViewControllers([inboxViewController], animated: false) } } diff --git a/FlowCrypt/Controllers/Inbox/InboxProviders.swift b/FlowCrypt/Controllers/Inbox/InboxProviders.swift index 0ad0b9437..bb4336d9d 100644 --- a/FlowCrypt/Controllers/Inbox/InboxProviders.swift +++ b/FlowCrypt/Controllers/Inbox/InboxProviders.swift @@ -14,7 +14,7 @@ struct InboxContext { } class InboxDataProvider { - func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { + func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { fatalError("Should be implemented") } } @@ -27,10 +27,10 @@ class InboxMessageThreadsProvider: InboxDataProvider { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { + override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { let result = try await provider.fetchThreads(using: context) let inboxData = result.threads.map { thread in - return InboxRenderable(thread: thread, folderPath: context.folderPath ?? "") + return InboxRenderable(thread: thread, folderPath: context.folderPath ?? "", activeUserEmail: userEmail) } let inboxContext = InboxContext( data: inboxData, @@ -44,13 +44,13 @@ class InboxMessageThreadsProvider: InboxDataProvider { class InboxMessageListProvider: InboxDataProvider { let provider: MessagesListProvider - init(provider: MessagesListProvider = MailProvider.shared.messageListProvider) { + init(provider: MessagesListProvider) { self.provider = provider } - override func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { + override func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { let result = try await provider.fetchMessages(using: context) - let inboxData = result.messages.map(InboxRenderable.init) + let inboxData = result.messages.map { InboxRenderable(message: $0) } let inboxContext = InboxContext( data: inboxData, pagination: result.pagination diff --git a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift index 7d888b117..6eeb8f3aa 100644 --- a/FlowCrypt/Controllers/Inbox/InboxRenderable.swift +++ b/FlowCrypt/Controllers/Inbox/InboxRenderable.swift @@ -23,6 +23,7 @@ struct InboxRenderable { let date: Date let wrappedType: WrappedType + } extension InboxRenderable { @@ -35,6 +36,7 @@ extension InboxRenderable { } extension InboxRenderable { + init(message: Message) { self.title = message.sender ?? "message_unknown_sender".localized self.messageCount = 1 @@ -45,9 +47,9 @@ extension InboxRenderable { self.wrappedType = .message(message) } - init(thread: MessageThread, folderPath: String) { + init(thread: MessageThread, folderPath: String, activeUserEmail: String) { - self.title = InboxRenderable.messageTitle(with: thread, and: folderPath) + self.title = InboxRenderable.messageTitle(activeUserEmail: activeUserEmail, with: thread, and: folderPath) self.messageCount = thread.messages.count self.subtitle = thread.subject ?? "message_missed_subject".localized @@ -64,9 +66,7 @@ extension InboxRenderable { self.wrappedType = .thread(thread) } - private static func messageTitle(with thread: MessageThread, and folderPath: String) -> String { - guard let myEmail = DataService.shared.email else { return "" } - + private static func messageTitle(activeUserEmail: String, with thread: MessageThread, and folderPath: String) -> String { // for now its not exactly clear how titles on other folders should looks like // so in scope of this PR we are applying this title presentation only for "sent" folder if folderPath == MessageLabelType.sent.value { @@ -74,7 +74,7 @@ extension InboxRenderable { // if we have only one email, it means that it could be "me" and we are not // clearing our own email from that if emails.count > 1 { - if let i = emails.firstIndex(of: myEmail) { + if let i = emails.firstIndex(of: activeUserEmail) { emails.remove(at: i) } } diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift index 8abf182ac..c54e60963 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController+Factory.swift @@ -11,28 +11,30 @@ import UIKit class InboxViewControllerFactory { @MainActor - static func make(with viewModel: InboxViewModel) -> InboxViewController { - guard let currentAuthType = DataService.shared.currentAuthType else { + static func make(appContext: AppContext, with viewModel: InboxViewModel) -> InboxViewController { + guard let currentAuthType = appContext.dataService.currentAuthType else { fatalError("Internal inconsistency") } switch currentAuthType { case .oAuthGmail: // Inject threads provider - Gmail API - guard let threadsProvider = MailProvider.shared.messagesThreadProvider else { + guard let threadsProvider = appContext.getRequiredMailProvider().messagesThreadProvider else { fatalError("Internal inconsistency") } return InboxViewController( + appContext: appContext, viewModel, numberOfInboxItemsToLoad: 20, // else timeouts happen provider: InboxMessageThreadsProvider(provider: threadsProvider) ) case .password: // Inject message list provider - IMAP - let provider = InboxMessageListProvider() + let provider = InboxMessageListProvider(provider: appContext.getRequiredMailProvider().messageListProvider) return InboxViewController( + appContext: appContext, viewModel, numberOfInboxItemsToLoad: 50, // safe to load 50, single call on IMAP provider: provider diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController.swift b/FlowCrypt/Controllers/Inbox/InboxViewController.swift index 5d33d6d11..bf27fcf47 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController.swift @@ -13,6 +13,8 @@ final class InboxViewController: ASDKViewController { private let numberOfInboxItemsToLoad: Int + private let appContext: AppContext + private let user: User private let service: ServiceActor private let decorator: InboxViewDecorator private let draftsListProvider: DraftsListProvider? @@ -32,17 +34,23 @@ final class InboxViewController: ASDKViewController { var path: String { viewModel.path } init( + appContext: AppContext, _ viewModel: InboxViewModel, numberOfInboxItemsToLoad: Int = 50, provider: InboxDataProvider, - draftsListProvider: DraftsListProvider? = MailProvider.shared.draftsProvider, + draftsListProvider: DraftsListProvider? = nil, decorator: InboxViewDecorator = InboxViewDecorator() ) { + guard let user = appContext.dataService.currentUser else { + fatalError("missing current user") // todo - DI user + } + self.user = user + self.appContext = appContext self.viewModel = viewModel self.numberOfInboxItemsToLoad = numberOfInboxItemsToLoad self.service = ServiceActor(inboxDataProvider: provider) - self.draftsListProvider = draftsListProvider + self.draftsListProvider = draftsListProvider ?? appContext.getRequiredMailProvider().draftsProvider self.decorator = decorator self.tableNode = TableNode() @@ -118,7 +126,7 @@ extension InboxViewController { // MARK: - Helpers extension InboxViewController { private func currentMessagesListPagination(from number: Int? = nil) -> MessagesListPagination { - MailProvider.shared.currentMessagesListPagination(from: number, token: state.token) + appContext.getRequiredMailProvider().currentMessagesListPagination(from: number, token: state.token) } private func messagesToLoad() -> Int { @@ -158,7 +166,7 @@ extension InboxViewController { ) ) let inboxContext = InboxContext( - data: context.messages.map(InboxRenderable.init), + data: context.messages.map { InboxRenderable(message: $0) }, pagination: context.pagination ) handleEndFetching(with: inboxContext, context: batchContext) @@ -176,7 +184,7 @@ extension InboxViewController { folderPath: viewModel.path, count: numberOfInboxItemsToLoad, pagination: currentMessagesListPagination() - ) + ), userEmail: user.email ) handleEndFetching(with: context, context: batchContext) } catch { @@ -198,7 +206,7 @@ extension InboxViewController { folderPath: viewModel.path, count: messagesToLoad(), pagination: pagination - ) + ), userEmail: user.email ) state = .fetched(context.pagination) handleEndFetching(with: context, context: batchContext) @@ -313,7 +321,7 @@ extension InboxViewController { } @objc private func handleSearchTap() { - let viewController = SearchViewController(folderPath: viewModel.path) + let viewController = SearchViewController(appContext: appContext, folderPath: viewModel.path) navigationController?.pushViewController(viewController, animated: false) } @@ -324,11 +332,8 @@ extension InboxViewController { } private func btnComposeTap() { - guard let email = DataService.shared.email else { - return - } TapTicFeedback.generate(.light) - let composeVc = ComposeViewController(email: email) + let composeVc = ComposeViewController(appContext: appContext) navigationController?.pushViewController(composeVc, animated: true) } } @@ -356,7 +361,7 @@ extension InboxViewController: ASTableDataSource, ASTableDelegate { func tableNode(_ tableNode: ASTableNode, didSelectRowAt indexPath: IndexPath) { tableNode.deselectRow(at: indexPath, animated: true) - open(with: inboxInput[indexPath.row], path: viewModel.path) + open(with: inboxInput[indexPath.row], path: viewModel.path, appContext: appContext) } // MARK: Cell Nodes @@ -455,7 +460,7 @@ private actor ServiceActor { self.inboxDataProvider = inboxDataProvider } - func fetchInboxItems(using context: FetchMessageContext) async throws -> InboxContext { - return try await inboxDataProvider.fetchInboxItems(using: context) + func fetchInboxItems(using context: FetchMessageContext, userEmail: String) async throws -> InboxContext { + return try await inboxDataProvider.fetchInboxItems(using: context, userEmail: userEmail) } } diff --git a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift index a8a399ec5..2f9196260 100644 --- a/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift +++ b/FlowCrypt/Controllers/MessageList Extension/MsgListViewController.swift @@ -10,7 +10,7 @@ import UIKit @MainActor protocol MsgListViewController { - func open(with message: InboxRenderable, path: String) + func open(with message: InboxRenderable, path: String, appContext: AppContext) func getUpdatedIndex(for message: InboxRenderable) -> Int? func updateMessage(isRead: Bool, at index: Int) @@ -18,39 +18,35 @@ protocol MsgListViewController { } extension MsgListViewController where Self: UIViewController { - func open(with message: InboxRenderable, path: String) { + + // todo - tom - don't know how to add AppContext into init of protocol/extension + func open(with message: InboxRenderable, path: String, appContext: AppContext) { switch message.wrappedType { case .message(let message): - openMsg(with: message, path: path) + openMsg(appContext: appContext, with: message, path: path) case .thread(let thread): - openThread(with: thread) + openThread(with: thread, appContext: appContext) } } // TODO: uncomment in "sent message from draft" feature - private func openDraft(with message: Message) { - guard let email = DataService.shared.email else { return } - - let controller = ComposeViewController(email: email) + private func openDraft(appContext: AppContext, with message: Message) { + let controller = ComposeViewController(appContext: appContext) controller.update(with: message) navigationController?.pushViewController(controller, animated: true) } - private func openMsg(with message: Message, path: String) { + private func openMsg(appContext: AppContext, with message: Message, path: String) { let thread = MessageThread(identifier: message.threadId, snippet: nil, path: path, messages: [message]) - openThread(with: thread) + openThread(with: thread, appContext: appContext) } - private func openThread(with thread: MessageThread) { - guard let threadOperationsProvider = MailProvider.shared.threadOperationsProvider else { - assertionFailure("Internal error. Provider should conform to MessagesThreadOperationsProvider") - return - } + private func openThread(with thread: MessageThread, appContext: AppContext) { let viewController = ThreadDetailsViewController( - threadOperationsProvider: threadOperationsProvider, + appContext: appContext, thread: thread ) { [weak self] (action, message) in self?.handleMessageOperation(with: message, action: action) diff --git a/FlowCrypt/Controllers/Search/SearchViewController.swift b/FlowCrypt/Controllers/Search/SearchViewController.swift index 4c906f304..fb986597a 100644 --- a/FlowCrypt/Controllers/Search/SearchViewController.swift +++ b/FlowCrypt/Controllers/Search/SearchViewController.swift @@ -41,16 +41,25 @@ final class SearchViewController: TableNodeViewController { // TODO: - https://github.com/FlowCrypt/flowcrypt-ios/issues/669 Adopt to gmail threads private let service: ServiceActor private var searchTask: DispatchWorkItem? - + private let appContext: AppContext private let searchController = UISearchController(searchResultsController: nil) private let folderPath: String private var searchedExpression: String = "" + private let currentUser: User init( - searchProvider: MessageSearchProvider = MailProvider.shared.messageSearchProvider, + appContext: AppContext, + searchProvider: MessageSearchProvider? = nil, folderPath: String ) { - self.service = ServiceActor(searchProvider: searchProvider) + guard let currentUser = appContext.dataService.currentUser else { + fatalError("no current user") // todo - use DI + } + self.currentUser = currentUser + self.appContext = appContext + self.service = ServiceActor( + searchProvider: searchProvider ?? appContext.getRequiredMailProvider().messageSearchProvider + ) self.folderPath = folderPath super.init(node: TableNode()) } @@ -111,6 +120,7 @@ extension SearchViewController { // MARK: - MessageHandlerViewConroller extension SearchViewController: MsgListViewController { + // TODO: - ANTON - check func getUpdatedIndex(for message: InboxRenderable) -> Int? { guard let message = message.wrappedMessage else { @@ -212,7 +222,7 @@ extension SearchViewController: ASTableDataSource, ASTableDelegate { guard let message = state.messages[safe: indexPath.row] else { return } // TODO: - https://github.com/FlowCrypt/flowcrypt-ios/issues/669 - cleanup - open(with: .init(message: message), path: folderPath) + open(with: .init(message: message), path: folderPath, appContext: appContext) } } diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift index 863e912fc..88930d293 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Option Scene/BackupOptionsViewController.swift @@ -32,20 +32,20 @@ final class BackupOptionsViewController: ASDKViewController { private var selectedOption: BackupOption = .email { didSet { handleOptionChange() } } - private let backupService: BackupServiceType private let service: ServiceActor private let userId: UserId + private let appContext: AppContext init( + appContext: AppContext, decorator: BackupOptionsViewDecoratorType = BackupOptionsViewDecorator(), - backupService: BackupServiceType = BackupService(), backups: [KeyDetails], userId: UserId ) { + self.appContext = appContext self.decorator = decorator self.backups = backups - self.backupService = backupService - self.service = ServiceActor(backupService: backupService) + self.service = ServiceActor(backupService: appContext.getBackupService()) self.userId = userId super.init(node: TableNode()) } @@ -87,6 +87,7 @@ extension BackupOptionsViewController { private func proceedToSelectBackupsScreen() { navigationController?.pushViewController( BackupSelectKeyViewController( + appContext: appContext, selectedOption: selectedOption, backups: backups, userId: userId @@ -119,7 +120,7 @@ extension BackupOptionsViewController { } private func backupAsFile() { - backupService.backupAsFile(keys: backups, for: self) + service.backupService.backupAsFile(keys: backups, for: self) } } @@ -181,7 +182,7 @@ extension BackupOptionsViewController: ASTableDelegate, ASTableDataSource { // TODO temporary solution for background execution problem private actor ServiceActor { - private let backupService: BackupServiceType + let backupService: BackupServiceType init(backupService: BackupServiceType) { self.backupService = backupService diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift index a77747db9..39556f822 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Scene/BackupViewController.swift @@ -35,18 +35,20 @@ final class BackupViewController: ASDKViewController { } } + private let appContext: AppContext private let decorator: BackupViewDecoratorType private let service: ServiceActor private let userId: UserId private var state: State = .idle { didSet { updateState() } } init( + appContext: AppContext, decorator: BackupViewDecoratorType = BackupViewDecorator(), - backupProvider: BackupServiceType = BackupService(), userId: UserId ) { + self.appContext = appContext self.decorator = decorator - self.service = ServiceActor(backupProvider: backupProvider) + self.service = ServiceActor(backupService: appContext.getBackupService()) self.userId = userId super.init(node: TableNode()) } @@ -132,7 +134,11 @@ extension BackupViewController: ASTableDelegate, ASTableDataSource { } private func proceedToBackupOptionsScreen() { - let optionsScreen = BackupOptionsViewController(backups: state.backups, userId: userId) + let optionsScreen = BackupOptionsViewController( + appContext: appContext, + backups: state.backups, + userId: userId + ) navigationController?.pushViewController(optionsScreen, animated: true) } } @@ -141,8 +147,8 @@ extension BackupViewController: ASTableDelegate, ASTableDataSource { private actor ServiceActor { private let backupProvider: BackupServiceType - init(backupProvider: BackupServiceType) { - self.backupProvider = backupProvider + init(backupService: BackupServiceType) { + self.backupProvider = backupService } func fetchBackupsFromInbox(for userId: UserId) async throws -> [KeyDetails] { diff --git a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift index 5aca0a769..50e5c4f91 100644 --- a/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift +++ b/FlowCrypt/Controllers/Settings/Backup/Backups Seleckt Key Scene/BackupSelectKeyViewController.swift @@ -12,7 +12,8 @@ import Foundation @MainActor final class BackupSelectKeyViewController: ASDKViewController { - private let backupService: BackupServiceType + + private let appContext: AppContext private let service: ServiceActor private let decorator: BackupSelectKeyDecoratorType private var backupsContext: [(KeyDetails, Bool)] @@ -20,20 +21,19 @@ final class BackupSelectKeyViewController: ASDKViewController { private let userId: UserId init( + appContext: AppContext, decorator: BackupSelectKeyDecoratorType = BackupSelectKeyDecorator(), - backupService: BackupServiceType = BackupService(), selectedOption: BackupOption, backups: [KeyDetails], userId: UserId ) { self.decorator = decorator - // set all selected bu default + // set all selected by default self.backupsContext = backups.map { ($0, true) } - self.backupService = backupService - self.service = ServiceActor(backupService: backupService) + self.service = ServiceActor(backupService: appContext.getBackupService()) self.selectedOption = selectedOption self.userId = userId - + self.appContext = appContext super.init(node: TableNode()) } @@ -100,7 +100,7 @@ extension BackupSelectKeyViewController { } private func backupAsFile() { - backupService.backupAsFile(keys: backupsContext.map(\.0), for: self) + service.backupService.backupAsFile(keys: backupsContext.map(\.0), for: self) } } @@ -134,7 +134,7 @@ extension BackupSelectKeyViewController: ASTableDelegate, ASTableDataSource { // TODO temporary solution for background execution problem private actor ServiceActor { - private let backupService: BackupServiceType + let backupService: BackupServiceType init(backupService: BackupServiceType) { self.backupService = backupService diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift index 9afe1e584..670c6605c 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift @@ -31,13 +31,13 @@ final class ContactDetailViewController: TableNodeViewController { private let action: ContactDetailAction? init( + appContext: AppContext, decorator: ContactDetailDecoratorType = ContactDetailDecorator(), - contactsProvider: LocalContactsProviderType = LocalContactsProvider(), recipient: RecipientWithSortedPubKeys, action: ContactDetailAction? ) { self.decorator = decorator - self.contactsProvider = contactsProvider + self.contactsProvider = LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) self.recipient = recipient self.action = action super.init(node: TableNode()) diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift index 8cd0ead34..5a51f1aee 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift @@ -17,16 +17,18 @@ import FlowCryptUI */ final class ContactsListViewController: TableNodeViewController { private let decorator: ContactsListDecoratorType - private let contactsProvider: LocalContactsProviderType + private let localContactsProvider: LocalContactsProviderType private var recipients: [RecipientWithSortedPubKeys] = [] private var selectedIndexPath: IndexPath? + private let appContext: AppContext init( - decorator: ContactsListDecoratorType = ContactsListDecorator(), - contactsProvider: LocalContactsProviderType = LocalContactsProvider() + appContext: AppContext, + decorator: ContactsListDecoratorType = ContactsListDecorator() ) { self.decorator = decorator - self.contactsProvider = contactsProvider + self.appContext = appContext + self.localContactsProvider = LocalContactsProvider(encryptedStorage: appContext.encryptedStorage) super.init(node: TableNode()) } @@ -64,7 +66,7 @@ extension ContactsListViewController { private func fetchContacts() { Task { do { - self.recipients = try await contactsProvider.getAllRecipients() + self.recipients = try await localContactsProvider.getAllRecipients() await self.node.reloadData() } catch { self.showToast("Failed to load recipients: \(error.localizedDescription)") @@ -105,6 +107,7 @@ extension ContactsListViewController { private func proceedToContactDetail(with indexPath: IndexPath) { let contactDetailViewController = ContactDetailViewController( + appContext: appContext, recipient: recipients[indexPath.row] ) { [weak self] action in guard case let .delete(contact) = action else { @@ -133,7 +136,7 @@ extension ContactsListViewController { recipientToRemove = recipients[indexPath.row] } - contactsProvider.remove(recipient: recipientToRemove) + localContactsProvider.remove(recipient: recipientToRemove) recipients.remove(at: indexPathToRemove.row) node.deleteRows(at: [indexPathToRemove], with: .left) } diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift index 8fae926bf..d13771f33 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift @@ -16,19 +16,22 @@ import FlowCryptUI * - User can see detail information for the key in *KeyDetailViewController* */ final class KeySettingsViewController: TableNodeViewController { + + private let appContext: AppContext private var keys: [KeyDetails] = [] private let decorator: KeySettingsViewDecorator - private let keyService: KeyServiceType private let isUsingKeyManager: Bool init( - decorator: KeySettingsViewDecorator = KeySettingsViewDecorator(), - keyService: KeyServiceType = KeyService(), - clientConfigurationService: ClientConfigurationServiceType = ClientConfigurationService() + appContext: AppContext, + decorator: KeySettingsViewDecorator = KeySettingsViewDecorator() ) { + self.appContext = appContext self.decorator = decorator - self.keyService = keyService - self.isUsingKeyManager = clientConfigurationService.getSavedForCurrentUser().isUsingKeyManager + guard let currentUser = appContext.dataService.currentUser else { + fatalError("missing current user") // todo - need more elegant solution + } + self.isUsingKeyManager = appContext.clientConfigurationService.getSaved(for: currentUser.email).isUsingKeyManager super.init(node: TableNode()) } @@ -71,14 +74,14 @@ final class KeySettingsViewController: TableNodeViewController { extension KeySettingsViewController { private func loadKeysFromStorageAndRender() async throws { - self.keys = try await keyService.getPrvKeyDetails() + self.keys = try await appContext.keyService.getPrvKeyDetails() await node.reloadData() } } extension KeySettingsViewController { @objc private func handleAddButtonTap() { - navigationController?.pushViewController(SetupManuallyImportKeyViewController(), animated: true) + navigationController?.pushViewController(SetupManuallyImportKeyViewController(appContext: appContext), animated: true) } } diff --git a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift index 513fda08f..d386b1791 100644 --- a/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/Settings List/SettingsViewController.swift @@ -43,19 +43,21 @@ final class SettingsViewController: TableNodeViewController { } } + private let appContext: AppContext private let decorator: SettingsViewDecoratorType - private let currentUser: User? private let clientConfiguration: ClientConfiguration private let rows: [SettingsMenuItem] init( - decorator: SettingsViewDecoratorType = SettingsViewDecorator(), - currentUser: User? = DataService.shared.currentUser, - clientConfigurationService: ClientConfigurationServiceType = ClientConfigurationService() + appContext: AppContext, + decorator: SettingsViewDecoratorType = SettingsViewDecorator() ) { + self.appContext = appContext self.decorator = decorator - self.currentUser = currentUser - self.clientConfiguration = clientConfigurationService.getSavedForCurrentUser() + guard let currentUser = appContext.dataService.currentUser?.email else { + fatalError("missing current user") // todo - need more elegant solution + } + self.clientConfiguration = appContext.clientConfigurationService.getSaved(for: currentUser) self.rows = SettingsMenuItem.filtered(with: self.clientConfiguration) super.init(node: TableNode()) } @@ -114,19 +116,23 @@ extension SettingsViewController { switch setting { case .keys: - viewController = KeySettingsViewController() + viewController = KeySettingsViewController( + appContext: appContext + ) case .legal: viewController = LegalViewController() case .contacts: - viewController = ContactsListViewController() + viewController = ContactsListViewController( + appContext: appContext + ) case .backups: - guard let currentUser = currentUser, + guard let currentUser = appContext.dataService.currentUser, clientConfiguration.canBackupKeys else { viewController = nil return } let userId = UserId(email: currentUser.email, name: currentUser.email) - viewController = BackupViewController(userId: userId) + viewController = BackupViewController(appContext: appContext, userId: userId) default: viewController = nil } diff --git a/FlowCrypt/Controllers/Setup/PassPhraseSaveable.swift b/FlowCrypt/Controllers/Setup/PassPhraseSaveable.swift index 2c054724f..e6e9a73f5 100644 --- a/FlowCrypt/Controllers/Setup/PassPhraseSaveable.swift +++ b/FlowCrypt/Controllers/Setup/PassPhraseSaveable.swift @@ -14,9 +14,6 @@ protocol PassPhraseSaveable { var passPhraseIndexes: [IndexPath] { get } var saveLocallyNode: CellNode { get } var saveInMemoryNode: CellNode { get } - - var passPhraseService: PassPhraseServiceType { get } - func handleSelectedPassPhraseOption() func showPassPhraseErrorAlert() } diff --git a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift index 02b4bd5eb..05d208115 100644 --- a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift @@ -21,14 +21,13 @@ final class SetupBackupsViewController: TableNodeViewController, PassPhraseSavea } private lazy var logger = Logger.nested(in: Self.self, with: .setup) + private let appContext: AppContext private let router: GlobalRouterType private let decorator: SetupViewDecorator private let core: Core private let keyMethods: KeyMethodsType private let user: UserId private let fetchedEncryptedKeys: [KeyDetails] - private let keyStorage: KeyStorageType - let passPhraseService: PassPhraseServiceType private var passPhrase: String? @@ -44,23 +43,21 @@ final class SetupBackupsViewController: TableNodeViewController, PassPhraseSavea } init( + appContext: AppContext, fetchedEncryptedKeys: [KeyDetails], router: GlobalRouterType = GlobalRouter(), - keyStorage: KeyStorageType = KeyDataStorage(), decorator: SetupViewDecorator = SetupViewDecorator(), core: Core = Core.shared, keyMethods: KeyMethodsType = KeyMethods(), - user: UserId, - passPhraseService: PassPhraseServiceType = PassPhraseService() + user: UserId ) { + self.appContext = appContext self.fetchedEncryptedKeys = fetchedEncryptedKeys self.router = router - self.keyStorage = keyStorage self.decorator = decorator self.core = core self.keyMethods = keyMethods self.user = user - self.passPhraseService = passPhraseService super.init(node: TableNode()) } @@ -143,10 +140,10 @@ extension SetupBackupsViewController { if storageMethod == .memory { for backup in matchingKeyBackups { let pp = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: backup.fingerprints) - passPhraseService.savePassPhrase(with: pp, storageMethod: storageMethod) + appContext.passPhraseService.savePassPhrase(with: pp, storageMethod: storageMethod) } } - keyStorage.addKeys( + appContext.keyStorage.addKeys( keyDetails: Array(matchingKeyBackups), passPhrase: storageMethod == .persistent ? passPhrase : nil, source: .backup, @@ -182,7 +179,7 @@ extension SetupBackupsViewController { } func handleBackButtonTap() { - router.signOut() + router.signOut(appContext: appContext) } private func moveToMainFlow() { diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift index 4aa3b5ed5..2fadaff24 100644 --- a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -16,6 +16,7 @@ import FlowCryptUI */ class SetupCreatePassphraseAbstractViewController: TableNodeViewController, PassPhraseSaveable, NavigationChildController { + enum Parts: Int, CaseIterable { case title, description, passPhrase, divider, saveLocally, saveInMemory, action, subtitle, fetchedKeys } @@ -24,14 +25,12 @@ class SetupCreatePassphraseAbstractViewController: TableNodeViewController, Pass Parts.allCases } + let appContext: AppContext let decorator: SetupViewDecorator - let core: Core + let core: Core = Core.shared let router: GlobalRouterType let user: UserId let fetchedKeysCount: Int - let storage: DataServiceType - let keyStorage: KeyStorageType - let passPhraseService: PassPhraseServiceType var storageMethod: StorageMethod = .persistent { didSet { @@ -49,23 +48,17 @@ class SetupCreatePassphraseAbstractViewController: TableNodeViewController, Pass private lazy var logger = Logger.nested(in: Self.self, with: .setup) init( + appContext: AppContext, user: UserId, fetchedKeysCount: Int = 0, - core: Core = .shared, router: GlobalRouterType = GlobalRouter(), - decorator: SetupViewDecorator = SetupViewDecorator(), - storage: DataServiceType = DataService.shared, - keyStorage: KeyStorageType = KeyDataStorage(), - passPhraseService: PassPhraseServiceType = PassPhraseService() + decorator: SetupViewDecorator = SetupViewDecorator() ) { + self.appContext = appContext self.user = user self.fetchedKeysCount = fetchedKeysCount - self.core = core self.router = router self.decorator = decorator - self.storage = storage - self.keyStorage = keyStorage - self.passPhraseService = passPhraseService super.init(node: TableNode()) } @@ -97,7 +90,7 @@ class SetupCreatePassphraseAbstractViewController: TableNodeViewController, Pass } func handleBackButtonTap() { - router.signOut() + router.signOut(appContext: self.appContext) } } diff --git a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift index 9b43ec81b..0238f4203 100644 --- a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift @@ -30,25 +30,19 @@ final class SetupEKMKeyViewController: SetupCreatePassphraseAbstractViewControll private let keys: [KeyDetails] init( + appContext: AppContext, user: UserId, keys: [KeyDetails] = [], - core: Core = .shared, router: GlobalRouterType = GlobalRouter(), - decorator: SetupViewDecorator = SetupViewDecorator(), - storage: DataServiceType = DataService.shared, - keyStorage: KeyStorageType = KeyDataStorage(), - passPhraseService: PassPhraseServiceType = PassPhraseService() + decorator: SetupViewDecorator = SetupViewDecorator() ) { self.keys = keys super.init( + appContext: appContext, user: user, fetchedKeysCount: keys.count, - core: core, router: router, - decorator: decorator, - storage: storage, - keyStorage: keyStorage, - passPhraseService: passPhraseService + decorator: decorator ) self.storageMethod = .memory } @@ -98,7 +92,7 @@ extension SetupEKMKeyViewController { passphrase: passPhrase ) let parsedKey = try await self.core.parseKeys(armoredOrBinary: encryptedPrv.encryptedKey.data()) - self.keyStorage.addKeys( + appContext.keyStorage.addKeys( keyDetails: parsedKey.keyDetails, passPhrase: self.storageMethod == .persistent ? passPhrase : nil, source: .ekm, @@ -108,7 +102,7 @@ extension SetupEKMKeyViewController { } if self.storageMethod == .memory { let passPhrase = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: allFingerprints.unique()) - self.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) + appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) } } } diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index 054ac25cc..3327da425 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -10,18 +10,6 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI -enum CreateKeyError: Error { - case weakPassPhrase(_ strength: CoreRes.ZxcvbnStrengthBar) - // Missing user email - case missedUserEmail - // Missing user name - case missedUserName - // Pass phrases don't match - case doesntMatch - // silent abort - case conformingPassPhraseError -} - /** * Controller which is responsible for generating a new key during setup * - User is sent here from **SetupInitialViewController** in case there are no backups found @@ -30,40 +18,30 @@ enum CreateKeyError: Error { */ final class SetupGenerateKeyViewController: SetupCreatePassphraseAbstractViewController { - private let backupService: BackupServiceType private let attester: AttesterApiType private let service: Service init( + appContext: AppContext, user: UserId, - backupService: BackupServiceType = BackupService(), - core: Core = .shared, router: GlobalRouterType = GlobalRouter(), - decorator: SetupViewDecorator = SetupViewDecorator(), - storage: DataServiceType = DataService.shared, - keyStorage: KeyStorageType = KeyDataStorage(), - attester: AttesterApiType = AttesterApi(), - passPhraseService: PassPhraseServiceType = PassPhraseService() + decorator: SetupViewDecorator = SetupViewDecorator() ) { - self.backupService = backupService - self.attester = attester + self.attester = AttesterApi( + clientConfiguration: appContext.clientConfigurationService.getSaved(for: user.email) + ) self.service = Service( + appContext: appContext, user: user, - backupService: backupService, - core: core, - keyStorage: keyStorage, - storage: storage, - attester: attester, - passPhraseService: passPhraseService + backupService: appContext.getBackupService(), + core: Core.shared, + attester: self.attester ) super.init( + appContext: appContext, user: user, - core: core, router: router, - decorator: decorator, - storage: storage, - keyStorage: keyStorage, - passPhraseService: passPhraseService + decorator: decorator ) } @@ -100,28 +78,24 @@ final class SetupGenerateKeyViewController: SetupCreatePassphraseAbstractViewCon private actor Service { typealias ViewController = SetupCreatePassphraseAbstractViewController + private let appContext: AppContext private let user: UserId private let backupService: BackupServiceType private let core: Core - private let keyStorage: KeyStorageType - private let storage: DataServiceType private let attester: AttesterApiType - private let passPhraseService: PassPhraseServiceType - - init(user: UserId, - backupService: BackupServiceType, - core: Core, - keyStorage: KeyStorageType, - storage: DataServiceType, - attester: AttesterApiType, - passPhraseService: PassPhraseServiceType) { + + init( + appContext: AppContext, + user: UserId, + backupService: BackupServiceType, + core: Core, + attester: AttesterApiType + ) { + self.appContext = appContext self.user = user self.backupService = backupService self.core = core - self.keyStorage = keyStorage - self.storage = storage self.attester = attester - self.passPhraseService = passPhraseService } func setupAccount(passPhrase: String, @@ -138,7 +112,7 @@ private actor Service { ) try await backupService.backupToInbox(keys: [encryptedPrv.key], for: user) - keyStorage.addKeys(keyDetails: [encryptedPrv.key], + appContext.keyStorage.addKeys(keyDetails: [encryptedPrv.key], passPhrase: storageMethod == .persistent ? passPhrase: nil, source: .generated, for: user.email) @@ -148,7 +122,7 @@ private actor Service { value: passPhrase, fingerprintsOfAssociatedKey: encryptedPrv.key.fingerprints ) - passPhraseService.savePassPhrase(with: passPhrase, storageMethod: .memory) + appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: .memory) } await submitKeyToAttesterAndShowAlertOnFailure( @@ -173,7 +147,7 @@ private actor Service { _ = try await attester.update( email: email, pubkey: publicKey, - token: storage.token + token: appContext.dataService.token ) } catch { let message = "Failed to submit Public Key" @@ -182,10 +156,10 @@ private actor Service { } private func getUserId() throws -> UserId { - guard let email = storage.email, !email.isEmpty else { + guard let email = appContext.dataService.email, !email.isEmpty else { throw CreateKeyError.missedUserEmail } - guard let name = storage.email, !name.isEmpty else { + guard let name = appContext.dataService.currentUser?.name, !name.isEmpty else { throw CreateKeyError.missedUserName } return UserId(email: email, name: name) diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index 8573d4122..2d4282b26 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -19,6 +19,7 @@ import FlowCryptUI * - Create new key - **SetupGenerateKeyViewController** */ final class SetupInitialViewController: TableNodeViewController { + private enum Parts: Int, CaseIterable { case title, description, createKey, importKey, anotherAccount } @@ -50,32 +51,31 @@ final class SetupInitialViewController: TableNodeViewController { .default } - private let backupService: BackupServiceType private let service: ServiceActor private let user: UserId private let router: GlobalRouterType private let decorator: SetupViewDecorator private let clientConfiguration: ClientConfiguration private let emailKeyManagerApi: EmailKeyManagerApiType + private let appContext: AppContext private lazy var logger = Logger.nested(in: Self.self, with: .setup) init( + appContext: AppContext, user: UserId, - backupService: BackupServiceType = BackupService(), router: GlobalRouterType = GlobalRouter(), decorator: SetupViewDecorator = SetupViewDecorator(), - clientConfigurationService: ClientConfigurationServiceType = ClientConfigurationService(), - emailKeyManagerApi: EmailKeyManagerApiType = EmailKeyManagerApi() + emailKeyManagerApi: EmailKeyManagerApiType? = nil ) { + self.appContext = appContext self.user = user - self.backupService = backupService - self.service = ServiceActor(backupService: backupService) + self.service = ServiceActor(backupService: appContext.getBackupService()) self.router = router self.decorator = decorator - self.clientConfiguration = clientConfigurationService.getSavedForCurrentUser() - self.emailKeyManagerApi = emailKeyManagerApi - + let clientConfiguration = appContext.clientConfigurationService.getSaved(for: user.email) + self.emailKeyManagerApi = emailKeyManagerApi ?? EmailKeyManagerApi(clientConfiguration: clientConfiguration) + self.clientConfiguration = clientConfiguration super.init(node: TableNode()) } @@ -135,7 +135,7 @@ extension SetupInitialViewController { } private func handleOtherAccount() { - router.signOut() + router.signOut(appContext: appContext) } private func handle(error: Error) { @@ -151,7 +151,8 @@ extension SetupInitialViewController { state = .searchingKeyBackupsInInbox case .inconsistentClientConfiguration(let error): showAlert(message: error.description) { [weak self] in - self?.router.signOut() + guard let self = self else { return } + self.router.signOut(appContext: self.appContext) } } } @@ -159,7 +160,7 @@ extension SetupInitialViewController { private func fetchKeysFromEKM() { Task { do { - let result = try await emailKeyManagerApi.getPrivateKeys() + let result = try await emailKeyManagerApi.getPrivateKeys(currentUserEmail: user.email) switch result { case .success(keys: let keys): proceedToSetupWithEKMKeys(keys: keys) @@ -170,12 +171,14 @@ extension SetupInitialViewController { self?.state = .fetchingKeysFromEKM }, onOk: { [weak self] in - self?.router.signOut() + guard let self = self else { return } + self.router.signOut(appContext: self.appContext) } ) case .keysAreNotDecrypted: showAlert(message: "organisational_rules_ekm_keys_are_not_decrypted_error".localized, onOk: { [weak self] in - self?.router.signOut() + guard let self = self else { return } + self.router.signOut(appContext: self.appContext) }) } } catch { @@ -313,16 +316,16 @@ extension SetupInitialViewController { // MARK: - Navigation extension SetupInitialViewController { private func proceedToKeyImport() { - let viewController = SetupManuallyImportKeyViewController() + let viewController = SetupManuallyImportKeyViewController(appContext: appContext) navigationController?.pushViewController(viewController, animated: true) } private func proceedToCreatingNewKey() { - let viewController = SetupGenerateKeyViewController(user: user) + let viewController = SetupGenerateKeyViewController(appContext: appContext, user: user) navigationController?.pushViewController(viewController, animated: true) } private func proceedToSetupWithEKMKeys(keys: [KeyDetails]) { - let viewController = SetupEKMKeyViewController(user: user, keys: keys) + let viewController = SetupEKMKeyViewController(appContext: appContext, user: user, keys: keys) navigationController?.pushViewController(viewController, animated: true) } @@ -334,7 +337,7 @@ extension SetupInitialViewController { state = .noKeyBackupsInInbox } else { logger.logInfo("\(keys.count) key backups found in inbox") - let viewController = SetupBackupsViewController(fetchedEncryptedKeys: keys, user: user) + let viewController = SetupBackupsViewController(appContext: appContext, fetchedEncryptedKeys: keys, user: user) navigationController?.pushViewController(viewController, animated: true) } } diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift index 1770a72eb..bc2c55858 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift @@ -23,14 +23,12 @@ final class SetupManuallyEnterPassPhraseViewController: TableNodeViewController, } } + private let appContext: AppContext private let decorator: SetupViewDecorator private let email: String private let fetchedKeys: [KeyDetails] private let keyMethods: KeyMethodsType - private let keysStorage: KeyStorageType - private let keyService: KeyServiceType private let router: GlobalRouterType - let passPhraseService: PassPhraseServiceType private var passPhrase: String? @@ -46,23 +44,19 @@ final class SetupManuallyEnterPassPhraseViewController: TableNodeViewController, } init( + appContext: AppContext, decorator: SetupViewDecorator = SetupViewDecorator(), keyMethods: KeyMethodsType = KeyMethods(), - keysService: KeyStorageType = KeyDataStorage(), router: GlobalRouterType = GlobalRouter(), - keyService: KeyServiceType = KeyService(), - passPhraseService: PassPhraseServiceType = PassPhraseService(), email: String, fetchedKeys: [KeyDetails] ) { + self.appContext = appContext self.fetchedKeys = fetchedKeys.unique() self.email = email self.decorator = decorator self.keyMethods = keyMethods - self.keysStorage = keysService self.router = router - self.keyService = keyService - self.passPhraseService = passPhraseService super.init(node: TableNode()) } @@ -233,7 +227,7 @@ extension SetupManuallyEnterPassPhraseViewController { showAlert(message: "setup_wrong_pass_phrase_retry".localized) return } - let keyDetails = try await keyService.getPrvKeyDetails() + let keyDetails = try await appContext.keyService.getPrvKeyDetails() importKeys(with: keyDetails, and: passPhrase) } @@ -241,8 +235,8 @@ extension SetupManuallyEnterPassPhraseViewController { let keysToUpdate = Array(Set(existedKeys).intersection(fetchedKeys)) let newKeysToAdd = Array(Set(fetchedKeys).subtracting(existedKeys)) - keysStorage.addKeys(keyDetails: newKeysToAdd, passPhrase: passPhrase, source: .imported, for: email) - keysStorage.updateKeys(keyDetails: keysToUpdate, passPhrase: passPhrase, source: .imported, for: email) + appContext.keyStorage.addKeys(keyDetails: newKeysToAdd, passPhrase: passPhrase, source: .imported, for: email) + appContext.keyStorage.updateKeys(keyDetails: keysToUpdate, passPhrase: passPhrase, source: .imported, for: email) if storageMethod == .memory { keysToUpdate @@ -250,7 +244,7 @@ extension SetupManuallyEnterPassPhraseViewController { PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { - passPhraseService.updatePassPhrase(with: $0, storageMethod: storageMethod) + appContext.passPhraseService.updatePassPhrase(with: $0, storageMethod: storageMethod) } newKeysToAdd @@ -258,7 +252,7 @@ extension SetupManuallyEnterPassPhraseViewController { PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { - passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) + appContext.passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) } } diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift index 3f6d31dd0..9fbfa5da5 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift @@ -28,9 +28,9 @@ final class SetupManuallyImportKeyViewController: TableNodeViewController { } } + private let appContext: AppContext private let decorator: SetupViewDecorator private let pasteboard: UIPasteboard - private let dataService: DataServiceType private let core: Core private var userInfoMessage = "" { @@ -38,14 +38,14 @@ final class SetupManuallyImportKeyViewController: TableNodeViewController { } init( + appContext: AppContext, decorator: SetupViewDecorator = SetupViewDecorator(), pasteboard: UIPasteboard = UIPasteboard.general, - core: Core = Core.shared, - dataService: DataServiceType = DataService.shared + core: Core = Core.shared ) { + self.appContext = appContext self.pasteboard = pasteboard self.decorator = decorator - self.dataService = dataService self.core = core super.init(node: TableNode()) } @@ -172,7 +172,7 @@ extension SetupManuallyImportKeyViewController { private func parseUserProvided(data keyData: Data) async throws { let keys = try await core.parseKeys(armoredOrBinary: keyData) let privateKey = keys.keyDetails.filter { $0.private != nil } - let user = dataService.email ?? "unknown_title".localized + let user = appContext.dataService.email ?? "unknown_title".localized if privateKey.isEmpty { userInfoMessage = "import_no_backups_clipboard".localized + user } else { @@ -183,6 +183,7 @@ extension SetupManuallyImportKeyViewController { private func proceedToPassPhrase(with email: String, keys: [KeyDetails]) { let viewController = SetupManuallyEnterPassPhraseViewController( + appContext: appContext, decorator: decorator, email: email, fetchedKeys: keys diff --git a/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift b/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift index 70db0bfa5..b57f9ed84 100644 --- a/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift +++ b/FlowCrypt/Controllers/Setup/SetupViewDecorator.swift @@ -83,9 +83,7 @@ struct SetupViewDecorator { case .choosingPassPhrase: subtitle = "" // "create_pass_phrase_description".localized https://github.com/FlowCrypt/flowcrypt-ios/issues/497 case .noBackups: - let user = DataService.shared.email ?? "unknown_title".localized - let msg = "setup_no_backups".localized + user - subtitle = msg + subtitle = "setup_no_backups".localized // todo - edit } return subtitle diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift index da403f40d..e947e535b 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift @@ -24,25 +24,25 @@ final class SetupImapViewController: TableNodeViewController { private var state: State = .idle private var selectedSection: Section? - private let dataService: DataServiceType private let globalRouter: GlobalRouterType + private let appContext: AppContext private let decorator: SetupImapViewDecorator private let sessionCredentials: SessionCredentialsProvider private let imap: Imap private var user = User.empty init( + appContext: AppContext, globalRouter: GlobalRouterType = GlobalRouter(), - dataService: DataServiceType = DataService.shared, decorator: SetupImapViewDecorator = SetupImapViewDecorator(), sessionCredentials: SessionCredentialsProvider = SessionCredentialsService(), - imap: Imap = Imap.shared + imap: Imap = Imap(user: User.empty) ) { + self.appContext = appContext self.globalRouter = globalRouter self.decorator = decorator self.sessionCredentials = sessionCredentials - self.dataService = dataService self.imap = imap super.init(node: TableNode()) @@ -529,7 +529,7 @@ extension SetupImapViewController { private func handleSuccessfulConnection() { hideSpinner() - globalRouter.signIn(with: .other(.session(user))) + globalRouter.signIn(appContext: self.appContext, route: .other(.session(user))) } private func checkCurrentUser() throws { diff --git a/FlowCrypt/Controllers/SideMenu/Main/SideMenuNavigationController.swift b/FlowCrypt/Controllers/SideMenu/Main/SideMenuNavigationController.swift index 453820549..856d3947a 100644 --- a/FlowCrypt/Controllers/SideMenu/Main/SideMenuNavigationController.swift +++ b/FlowCrypt/Controllers/SideMenu/Main/SideMenuNavigationController.swift @@ -47,8 +47,8 @@ final class SideMenuNavigationController: ENSideMenuNavigationController { private var menuViewContoller: SideMenuViewController? - convenience init(contentViewController: UIViewController) { - let menu = MyMenuViewController() + convenience init(appContext: AppContext, contentViewController: UIViewController) { + let menu = MyMenuViewController(appContext: appContext) self.init(menuViewController: menu, contentViewController: contentViewController) menuViewContoller = menu sideMenu = ENSideMenu(sourceView: view, menuViewController: menu, menuPosition: .left).then { diff --git a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift index 62ea7075f..972f9d1d6 100644 --- a/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift +++ b/FlowCrypt/Controllers/SideMenu/Menu/MyMenuViewController.swift @@ -43,14 +43,15 @@ final class MyMenuViewController: ASDKViewController { } } - private let foldersProvider: FoldersServiceType - private let dataService: DataServiceType + private let appContext: AppContext + private let foldersService: FoldersServiceType private let router: GlobalRouterType private let decorator: MyMenuViewDecoratorType private var folders: [FolderViewModel] = [] private var serviceItems: [FolderViewModel] { FolderViewModel.menuItems } - private var accounts: [User] { dataService.validAccounts() } + private var accounts: [User] { appContext.dataService.validAccounts() } + private let currentUser: User private let tableNode: ASTableNode @@ -64,14 +65,17 @@ final class MyMenuViewController: ASDKViewController { private var isFirstLaunch = true init( - foldersProvider: FoldersServiceType = FoldersService(), - dataService: DataServiceType = DataService.shared, + appContext: AppContext, globalRouter: GlobalRouterType = GlobalRouter(), decorator: MyMenuViewDecoratorType = MyMenuViewDecorator(), tableNode: ASTableNode = TableNode() ) { - self.foldersProvider = foldersProvider - self.dataService = dataService + guard let currentUser = appContext.dataService.currentUser else { + fatalError("no current user") // todo - dependency-inject + } + self.currentUser = currentUser + self.appContext = appContext + self.foldersService = appContext.getFoldersService() self.router = globalRouter self.decorator = decorator self.tableNode = tableNode @@ -173,7 +177,7 @@ extension MyMenuViewController { showSpinner() Task { do { - let folders = try await foldersProvider.fetchFolders(isForceReload: true) + let folders = try await foldersService.fetchFolders(isForceReload: true, for: self.currentUser) handleNewFolders(with: folders) } catch { handleError(with: error) @@ -209,7 +213,7 @@ extension MyMenuViewController { // MARK: - Account functionality extension MyMenuViewController { private func addAccount() { - let vc = MainNavigationController(rootViewController: SignInViewController()) + let vc = MainNavigationController(rootViewController: SignInViewController(appContext: appContext)) present(vc, animated: true, completion: nil) } @@ -218,7 +222,7 @@ extension MyMenuViewController { return } - router.switchActive(user: account) + router.switchActive(user: account, appContext: appContext) } } @@ -244,7 +248,7 @@ extension MyMenuViewController { switch (section, state) { case (.header, _): let headerInput = decorator.header( - for: dataService.currentUser, + for: appContext.dataService.currentUser, image: state.arrowImage ) return TextImageNode(input: headerInput) { [weak self] node in @@ -279,7 +283,7 @@ extension MyMenuViewController { switch folder.itemType { case .folder: let input = InboxViewModel(folder) - let viewController = InboxViewControllerFactory.make(with: input) + let viewController = InboxViewControllerFactory.make(appContext: appContext, with: input) if let topController = topController(controllerType: InboxViewController.self), topController.path == folder.path { @@ -293,9 +297,9 @@ extension MyMenuViewController { sideMenuController()?.sideMenu?.hideSideMenu() return } - sideMenuController()?.setContentViewController(SettingsViewController()) + sideMenuController()?.setContentViewController(SettingsViewController(appContext: appContext)) case .logOut: - router.signOut() + router.signOut(appContext: appContext) } } diff --git a/FlowCrypt/Controllers/SignIn/SignInViewController.swift b/FlowCrypt/Controllers/SignIn/SignInViewController.swift index 3fff2af49..92e5fd551 100644 --- a/FlowCrypt/Controllers/SignIn/SignInViewController.swift +++ b/FlowCrypt/Controllers/SignIn/SignInViewController.swift @@ -24,15 +24,18 @@ final class SignInViewController: TableNodeViewController { case links, logo, description, gmail, outlook, other } + private let appContext: AppContext private let globalRouter: GlobalRouterType private let core: Core private let decorator: SignInViewDecoratorType init( + appContext: AppContext, globalRouter: GlobalRouterType = GlobalRouter(), core: Core = Core.shared, decorator: SignInViewDecoratorType = SignInViewDecorator() ) { + self.appContext = appContext self.globalRouter = globalRouter self.core = core self.decorator = decorator @@ -116,7 +119,7 @@ extension SignInViewController: ASTableDelegate, ASTableDataSource { extension SignInViewController { private func signInWithGmail() { - globalRouter.signIn(with: .gmailLogin(self)) + globalRouter.signIn(appContext: appContext, route: .gmailLogin(self)) } private func signInWithOutlook() { @@ -124,7 +127,7 @@ extension SignInViewController { } private func proceedToOtherProvider() { - let setupViewController = SetupImapViewController() + let setupViewController = SetupImapViewController(appContext: appContext) navigationController?.pushViewController(setupViewController, animated: true) } diff --git a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift index f751229be..55687efa9 100644 --- a/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift +++ b/FlowCrypt/Controllers/Threads/MessageActionsHandler.swift @@ -30,11 +30,11 @@ extension MessageActionsHandler where Self: UIViewController { Logger.nested("MessageActions") } - func setupNavigationBar() { + func setupNavigationBar(user: User) { Task { do { let path = try await trashFolderProvider.getTrashFolderPath() - setupNavigationBarItems(with: path) + setupNavigationBarItems(with: path, user: user) } catch { // todo - handle? logger.logError("setupNavigationBar: \(error)") @@ -42,7 +42,7 @@ extension MessageActionsHandler where Self: UIViewController { } } - private func setupNavigationBarItems(with trashFolderPath: String?) { + private func setupNavigationBarItems(with trashFolderPath: String?, user: User) { logger.logInfo("setup navigation bar with \(trashFolderPath ?? "N/A")") logger.logInfo("currentFolderPath \(currentFolderPath)") diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 410627dae..279e86cf2 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -30,12 +30,14 @@ final class ThreadDetailsViewController: TableNodeViewController { case thread, message } + private let appContext: AppContext private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider private let thread: MessageThread private let filesManager: FilesManagerType private var input: [ThreadDetailsViewController.Input] + private let user: User let trashFolderProvider: TrashFolderProviderType var currentFolderPath: String { @@ -49,18 +51,41 @@ final class ThreadDetailsViewController: TableNodeViewController { ) init( - messageService: MessageService = MessageService(), - trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), - messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, - threadOperationsProvider: MessagesThreadOperationsProvider, + appContext: AppContext, + messageService: MessageService? = nil, thread: MessageThread, filesManager: FilesManagerType = FilesManager(), completion: @escaping MessageActionCompletion ) { - self.messageService = messageService + self.appContext = appContext + guard let user = appContext.dataService.currentUser else { + fatalError("expected current user to exist") // todo - better accept user as VC argument + } + self.user = user + let clientConfiguration = appContext.clientConfigurationService.getSaved(for: user.email) + self.messageService = messageService ?? MessageService( + contactsService: ContactsService( + localContactsProvider: LocalContactsProvider( + encryptedStorage: appContext.encryptedStorage + ), + clientConfiguration: clientConfiguration + ), + keyService: appContext.keyService, + messageProvider: appContext.getRequiredMailProvider().messageProvider, + passPhraseService: appContext.passPhraseService + ) + guard let threadOperationsProvider = appContext.getRequiredMailProvider().threadOperationsProvider else { + fatalError("expected threadOperationsProvider on gmail") + } self.threadOperationsProvider = threadOperationsProvider - self.messageOperationsProvider = messageOperationsProvider - self.trashFolderProvider = trashFolderProvider + self.messageOperationsProvider = appContext.getRequiredMailProvider().messageOperationsProvider + self.trashFolderProvider = TrashFolderProvider( + user: user, + folderProvider: FoldersService( + encryptedStorage: appContext.encryptedStorage, + remoteFoldersProvider: appContext.getRequiredMailProvider().remoteFoldersProvider + ) + ) self.thread = thread self.filesManager = filesManager self.onComplete = completion @@ -81,7 +106,7 @@ final class ThreadDetailsViewController: TableNodeViewController { node.delegate = self node.dataSource = self - setupNavigationBar() + setupNavigationBar(user: user) expandThreadMessage() } } @@ -142,8 +167,7 @@ extension ThreadDetailsViewController { } private func composeNewMessage(at indexPath: IndexPath, quoteType: MessageQuoteType) { - guard let email = DataService.shared.email, - let input = input[safe: indexPath.section-1], + guard let input = input[safe: indexPath.section-1], let processedMessage = input.processedMessage else { return } @@ -171,7 +195,7 @@ extension ThreadDetailsViewController { let composeInput = ComposeMessageInput(type: .quote(replyInfo)) navigationController?.pushViewController( - ComposeViewController(email: email, input: composeInput), + ComposeViewController(appContext: appContext, input: composeInput), animated: true ) } @@ -350,9 +374,10 @@ extension ThreadDetailsViewController { } extension ThreadDetailsViewController: MessageActionsHandler { + private func handleSuccessfulMessage(action: MessageAction) { hideSpinner() - onComplete(action, .init(thread: thread, folderPath: currentFolderPath)) + onComplete(action, .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: user.email)) navigationController?.popViewController(animated: true) } @@ -488,7 +513,7 @@ extension ThreadDetailsViewController: NavigationChildController { func handleBackButtonTap() { let isRead = input.contains(where: { $0.rawMessage.isMessageRead }) logger.logInfo("Back button. Are all messages read \(isRead) ") - onComplete(MessageAction.markAsRead(isRead), .init(thread: thread, folderPath: currentFolderPath)) + onComplete(MessageAction.markAsRead(isRead), .init(thread: thread, folderPath: currentFolderPath, activeUserEmail: self.user.email)) navigationController?.popViewController(animated: true) } } diff --git a/FlowCrypt/Extensions/UIApplicationExtension.swift b/FlowCrypt/Extensions/UIApplicationExtension.swift index 00b44491c..7464da5f2 100644 --- a/FlowCrypt/Extensions/UIApplicationExtension.swift +++ b/FlowCrypt/Extensions/UIApplicationExtension.swift @@ -23,4 +23,7 @@ extension UIApplication { .first(where: \.isKeyWindow) } + var isRunningTests: Bool { + NSClassFromString("XCTestCase") != nil + } } diff --git a/FlowCrypt/Functionality/DataManager/DataService.swift b/FlowCrypt/Functionality/DataManager/DataService.swift index ceacbec1e..e59e7f148 100644 --- a/FlowCrypt/Functionality/DataManager/DataService.swift +++ b/FlowCrypt/Functionality/DataManager/DataService.swift @@ -9,11 +9,10 @@ import Foundation import RealmSwift -protocol EmailProviderType { - var email: String? { get } -} -protocol DataServiceType: EmailProviderType { +// todo DataServiceType in general is a bit of a confused class +// hopefully we can refactor it away or shrink it +protocol DataServiceType { // data var email: String? { get } var currentUser: User? { get } @@ -25,11 +24,8 @@ protocol DataServiceType: EmailProviderType { var users: [User] { get } func validAccounts() -> [User] -} - -protocol ImapSessionProvider { - func imapSession() -> IMAPSession? - func smtpSession() -> SMTPSession? + + func performMigrationIfNeeded() async throws } enum SessionType: CustomStringConvertible { @@ -48,14 +44,13 @@ enum SessionType: CustomStringConvertible { // MARK: - DataService final class DataService { - static let shared = DataService() private let encryptedStorage: EncryptedStorageType private let localStorage: LocalStorageType private let migrationService: DBMigration - private init( - encryptedStorage: EncryptedStorageType = EncryptedStorage(), + init( + encryptedStorage: EncryptedStorageType, localStorage: LocalStorageType = LocalStorage() ) { self.encryptedStorage = encryptedStorage @@ -105,7 +100,10 @@ extension DataService: DataServiceType { var token: String? { switch currentAuthType { case .oAuthGmail: - return GoogleUserService().userToken + return GoogleUserService( + currentUserEmail: currentUser?.email, + appDelegateGoogleSessionContainer: nil // needed only when signing in/out + ).userToken default: return nil } @@ -125,34 +123,3 @@ extension DataService: DBMigration { try await migrationService.performMigrationIfNeeded() } } - -// MARK: - SessionProvider -extension DataService: ImapSessionProvider { - func imapSession() -> IMAPSession? { - guard let user = activeUser else { - assertionFailure("Can't get IMAP Session without user data") - return nil - } - - guard let imapSession = IMAPSession(user: user) else { - assertionFailure("couldn't create IMAP Session with this parameters") - return nil - } - - return imapSession - } - - func smtpSession() -> SMTPSession? { - guard let user = activeUser else { - assertionFailure("Can't get SMTP Session without user data") - return nil - } - - guard let smtpSession = SMTPSession(user: user) else { - assertionFailure("couldn't create SMTP Session with this parameters") - return nil - } - - return smtpSession - } -} diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index bc00d026c..fb9881617 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -7,8 +7,8 @@ // import FlowCryptCommon -import Foundation import RealmSwift +import UIKit // swiftlint:disable force_try protocol EncryptedStorageType: KeyStorageType { @@ -51,13 +51,13 @@ final class EncryptedStorage: EncryptedStorageType { static let encryptedDbFilename = "encrypted.realm" } - private let keychainService: KeyChainServiceType - private lazy var migrationLogger = Logger.nested(in: Self.self, with: .migration) private lazy var logger = Logger.nested(Self.self) private let currentSchema: EncryptedStorageSchema = .version5 private let supportedSchemas = EncryptedStorageSchema.allCases + + private let storageEncryptionKey: Data var storage: Realm { do { @@ -70,8 +70,8 @@ final class EncryptedStorage: EncryptedStorageType { } } - init(keychainHelper: KeyChainServiceType = KeyChainService()) { - self.keychainService = KeyChainService() + init(storageEncryptionKey: Data) { + self.storageEncryptionKey = storageEncryptionKey } private func getDocumentDirectory() -> String { @@ -82,13 +82,16 @@ final class EncryptedStorage: EncryptedStorageType { } private func getConfiguration() throws -> Realm.Configuration { + guard !UIApplication.shared.isRunningTests else { + return Realm.Configuration(inMemoryIdentifier: UUID().uuidString) + } + let path = getDocumentDirectory() + "/" + Constants.encryptedDbFilename - let key = try keychainService.getStorageEncryptionKey() let latestSchemaVersion = currentSchema.version.dbSchemaVersion return Realm.Configuration( fileURL: URL(fileURLWithPath: path), - encryptionKey: key, + encryptionKey: storageEncryptionKey, schemaVersion: latestSchemaVersion, migrationBlock: { [weak self] migration, oldSchemaVersion in self?.performSchemaMigration(migration: migration, from: oldSchemaVersion, to: latestSchemaVersion) diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift index 5956d8446..cf67f9fc2 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/KeyChainService.swift @@ -10,64 +10,60 @@ import FlowCryptCommon import Foundation import Security -// keychain is used to generate and retrieve encryption key which is used to encrypt local DB -// it does not contain any actual data or keys other than the db encryption key +/// keychain is used to generate and retrieve encryption key which is used to encrypt local DB +/// it does not contain any actual data or keys other than the db encryption key +/// index of the keychain entry is dynamic (set up once per app installation), set in user defaults +actor KeyChainService { -protocol KeyChainServiceType { - func getStorageEncryptionKey() throws -> Data -} - -struct KeyChainService: KeyChainServiceType { - private static var logger = Logger.nested(in: Self.self, with: "Keychain") + private let logger = Logger.nested("KeyChain") + private let keyByteLen = 64 - // the prefix ensures that we use a different keychain index after deleting the app - // because keychain entries survive app uninstall - private static var keychainIndex: String = { - // todo - verify if this is indeed atomic (because static) or if there can be a race condition - let prefixStorageIndex = "indexSecureKeychainPrefix" - let storageEncryptionKeyIndexSuffix = "-indexStorageEncryptionKey" - if let storedPrefix = UserDefaults.standard.string(forKey: prefixStorageIndex) { - return storedPrefix + storageEncryptionKeyIndexSuffix + /// this dynamic keychainIndex ensures that we use a different keychain index + /// after deleting the app, because keychain entries survive app uninstall + @MainActor private func getKeychainIndex() throws -> String { + let dynamicPartIndex = "indexSecureKeychainPrefix" + if let storedDynamicPart = UserDefaults.standard.string(forKey: dynamicPartIndex) { + return constructKeychainIndex(dynamicPart: storedDynamicPart) } + let newDynamicPart = try newRandomString() + UserDefaults.standard.set(newDynamicPart, forKey: dynamicPartIndex) + return constructKeychainIndex(dynamicPart: newDynamicPart) + } + + @MainActor private func newRandomString() throws -> String { + logger.logInfo("newRandomString - generating new KeyChain index") guard let randomBytes = CoreHost().getSecureRandomByteNumberArray(12) else { - fatalError("could not get secureKeychainPrefix random bytes") + throw AppErr.general("KeyChainService.newRandomString - randomBytes are nil") } - let prefix = Data(randomBytes) + return Data(randomBytes) .base64EncodedString() .replacingOccurrences(of: "[^A-Za-z0-9]+", with: "", options: [.regularExpression]) + } - logger.logInfo("LocalStorage.secureKeychainPrefix generating new prefix") - UserDefaults.standard.set(prefix, forKey: prefixStorageIndex) - return prefix + storageEncryptionKeyIndexSuffix - }() - - private let keyByteLen = 64 - - private func generateAndSaveStorageEncryptionKey() throws { - Self.logger.logInfo("generateAndSaveStorageEncryptionKey") + @MainActor private func constructKeychainIndex(dynamicPart: String) -> String { + return dynamicPart + "-indexStorageEncryptionKey" + } + @MainActor private func generateAndSaveStorageEncryptionKey() throws { + logger.logInfo("generateAndSaveStorageEncryptionKey") guard let randomBytes = CoreHost().getSecureRandomByteNumberArray(keyByteLen) else { - let message = "KeyChainService getSecureRandomByteNumberArray bytes are nil" - throw AppErr.general(message) + throw AppErr.general("KeyChainService getSecureRandomByteNumberArray bytes are nil") } - - let key = Data(randomBytes) let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, - kSecAttrAccount: KeyChainService.keychainIndex, - kSecValueData: key + kSecAttrAccount: try getKeychainIndex(), + kSecValueData: Data(randomBytes) ] let addOsStatus = SecItemAdd(query as CFDictionary, nil) guard addOsStatus == noErr else { - let message = "KeyChainService SecItemAdd osStatus = \(addOsStatus), expected 'noErr'" - throw AppErr.general(message) + throw AppErr.general("KeyChainService SecItemAdd osStatus = \(addOsStatus), expected 'noErr'") } } - func getStorageEncryptionKey() throws -> Data { + @MainActor func getStorageEncryptionKey() throws -> Data { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, - kSecAttrAccount: KeyChainService.keychainIndex, + kSecAttrAccount: try getKeychainIndex(), kSecReturnData: kCFBooleanTrue!, kSecMatchLimit: kSecMatchLimitOne ] @@ -79,18 +75,15 @@ struct KeyChainService: KeyChainServiceType { } guard findOsStatus == noErr else { - let message = "KeyChainService SecItemCopyMatching status = \(findOsStatus), expected 'noErr'" - throw AppErr.general(message) + throw AppErr.general("KeyChainService SecItemCopyMatching status = \(findOsStatus), expected 'noErr'") } guard let validKey = keyFromKeychain as? Data else { - let message = "KeyChainService keyFromKeychain not usable as Data. Is nil?: \(keyFromKeychain == nil)" - throw AppErr.general(message) + throw AppErr.general("KeyChainService keyFromKeychain not usable as Data. Is nil?: \(keyFromKeychain == nil)") } guard validKey.count == keyByteLen else { - let message = "KeyChainService validKey.count != \(keyByteLen), instead is \(validKey.count)" - throw AppErr.general(message) + throw AppErr.general("KeyChainService validKey.count != \(keyByteLen), instead is \(validKey.count)") } return validKey diff --git a/FlowCrypt/Functionality/DataManager/UserAccountService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift similarity index 69% rename from FlowCrypt/Functionality/DataManager/UserAccountService.swift rename to FlowCrypt/Functionality/DataManager/SessionService.swift index 9891a1a83..e9f4beea6 100644 --- a/FlowCrypt/Functionality/DataManager/UserAccountService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -9,40 +9,38 @@ import FlowCryptCommon import Foundation -protocol UserAccountServiceType { - func startSessionFor(user type: SessionType) +protocol SessionServiceType { + func startSessionFor(session: SessionType) func switchActiveSessionFor(user: User) -> SessionType? func startActiveSessionForNextUser() -> SessionType? func cleanupSessions() func cleanup() } -final class UserAccountService { +final class SessionService { private let encryptedStorage: EncryptedStorageType & LogOutHandler private let localStorage: LocalStorageType & LogOutHandler - private let dataService: DataServiceType private let imap: Imap private let googleService: GoogleUserService + private let dataService: DataService private lazy var logger = Logger.nested(Self.self) init( - encryptedStorage: EncryptedStorageType & LogOutHandler = EncryptedStorage(), + encryptedStorage: EncryptedStorageType & LogOutHandler, localStorage: LocalStorageType & LogOutHandler = LocalStorage(), - dataService: DataServiceType = DataService.shared, - imap: Imap = .shared, - googleService: GoogleUserService = GoogleUserService() + dataService: DataService, + imap: Imap? = nil, + googleService: GoogleUserService ) { + self.googleService = googleService + // todo - the following User.empty may be wrong - unsure, untested + // maybe should instead get user + self.imap = imap ?? Imap(user: dataService.currentUser ?? User.empty) self.encryptedStorage = encryptedStorage self.localStorage = localStorage self.dataService = dataService - self.imap = imap - self.googleService = googleService - } - - private var currentUser: User? { - dataService.currentUser } private var storages: [LogOutHandler] { @@ -50,12 +48,11 @@ final class UserAccountService { } } -extension UserAccountService: UserAccountServiceType { +extension SessionService: SessionServiceType { /// start session for a user, this method will log out current user if user was saved, save and start session for a new user - func startSessionFor(user type: SessionType) { - switch type { + func startSessionFor(session: SessionType) { + switch session { case let .google(email, name, token): - // save new user data let user = User.googleUser( name: name, email: email, @@ -63,14 +60,16 @@ extension UserAccountService: UserAccountServiceType { ) encryptedStorage.saveActiveUser(with: user) case let .session(user): - encryptedStorage.saveActiveUser(with: user) - // start session for saved user imap.setupSession() + encryptedStorage.saveActiveUser(with: user) } } func startActiveSessionForNextUser() -> SessionType? { - logOutCurrentUser() + guard let currentUser = dataService.currentUser else { + return nil + } + logOut(user: currentUser) guard let nextUser = encryptedStorage.getAllUsers().first else { return nil @@ -94,21 +93,16 @@ extension UserAccountService: UserAccountServiceType { return switchActiveSession(for: currentUser) } + // todo - rename to "logOutUsersThatDontHaveAnyKeysSetUp" func cleanupSessions() { logger.logInfo("Clean up sessions") - - encryptedStorage.getAllUsers() - .filter { - !encryptedStorage.doesAnyKeyExist(for: $0.email) - } - .map { - logger.logInfo("User session to clean up \($0.email)") - return $0.email + for user in encryptedStorage.getAllUsers() { + if !encryptedStorage.doesAnyKeyExist(for: user.email) { + logger.logInfo("User session to clean up \(user.email)") + logOut(user: user) } - .forEach(logOut) - + } let users = encryptedStorage.getAllUsers() - if !users.contains(where: { $0.isActive }), let user = users.first(where: { encryptedStorage.doesAnyKeyExist(for: $0.email ) }) { switchActiveSession(for: user) } @@ -129,33 +123,25 @@ extension UserAccountService: UserAccountServiceType { return nil } - startSessionFor(user: sessionType) + startSessionFor(session: sessionType) return sessionType } - private func logOutCurrentUser() { - guard let email = dataService.currentUser?.email else { - logger.logWarning("User is not logged in. Can't log out") - return - } - logOut(user: email) - } - - private func logOut(user email: String) { - logger.logInfo("Logging out user \(email)") - - switch dataService.currentAuthType { + private func logOut(user: User) { + logger.logInfo("Logging out user \(user.email)") + switch user.authType { case .oAuthGmail: - googleService.signOut(user: email) + googleService.signOut(user: user.email) case .password: imap.disconnect() default: logger.logWarning("currentAuthType is not resolved") } - do { - try self.storages.forEach { try $0.logOutUser(email: email) } + for storage in self.storages { + try storage.logOutUser(email: user.email) + } } catch { logger.logError("storage error \(error)") } diff --git a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift index 9e8da7c42..059befeab 100644 --- a/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift +++ b/FlowCrypt/Functionality/Error Handling/KeyServiceErrorHandler.swift @@ -8,6 +8,18 @@ import UIKit +enum CreateKeyError: Error { + case weakPassPhrase(_ strength: CoreRes.ZxcvbnStrengthBar) + // Missing user email + case missedUserEmail + // Missing user name + case missedUserName + // Pass phrases don't match + case doesntMatch + // silent abort + case conformingPassPhraseError +} + // KeyServiceError struct KeyServiceErrorHandler: ErrorHandler { func handle(error: Error, for viewController: UIViewController) -> Bool { diff --git a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift index 172ade3d7..f40321669 100644 --- a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift @@ -67,9 +67,8 @@ final class UserContactsProvider { return contactsScope.allSatisfy(currentScope.contains) } - init(userService: GoogleUserServiceType & UserServiceType = GoogleUserService()) { + init(userService: GoogleUserServiceType & UserServiceType) { self.userService = userService - runWarmupQuery() } diff --git a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift index dc0407985..bc8f3e146 100644 --- a/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Gmail/GmailService.swift @@ -11,6 +11,7 @@ import Foundation import GoogleAPIClientForREST_Gmail class GmailService: MailServiceProvider { + let mailServiceProviderType = MailServiceProviderType.gmail let userService: GoogleUserServiceType let backupSearchQueryProvider: GmailBackupSearchQueryProviderType @@ -35,7 +36,8 @@ class GmailService: MailServiceProvider { var progressHandler: ((Float) -> Void)? init( - userService: GoogleUserServiceType = GoogleUserService(), + currentUserEmail: String, + userService: GoogleUserServiceType, backupSearchQueryProvider: GmailBackupSearchQueryProviderType = GmailBackupSearchQueryProvider() ) { self.userService = userService diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift index 27a2b1b6c..a0e18a044 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+msg.swift @@ -6,6 +6,7 @@ import Foundation import MailCore extension Imap { + func fetchMsg(message: MCOIMAPMessage, folder: String) async throws -> Data { return try await execute("fetchMsg", { sess, respond in sess.fetchMessageOperation( diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index ccc00e212..aaaa726d6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -14,8 +14,8 @@ extension Imap { func setupSession() { guard - let imapSession = dataService.imapSession(), - let smtpSession = dataService.smtpSession() + let imapSession = imapSessionProvider.imapSession(), + let smtpSession = imapSessionProvider.smtpSession() else { return } logger.logInfo("Creating a new IMAP session") let newImapSession = MCOIMAPSession(session: imapSession) diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap.swift index 302cfebb4..a1164250d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap.swift @@ -6,11 +6,10 @@ import FlowCryptCommon import MailCore final class Imap: MailServiceProvider { - let mailServiceProviderType = MailServiceProviderType.imap - typealias Injection = ImapSessionProvider & DataServiceType - static let shared: Imap = Imap() + let mailServiceProviderType = MailServiceProviderType.imap + let user: User let helper: ImapHelperType let messageKindProvider: MessageKindProviderType var imapSess: MCOIMAPSession? @@ -20,16 +19,17 @@ final class Imap: MailServiceProvider { typealias ReqKind = MCOIMAPMessagesRequestKind typealias Err = MCOErrorCode - let dataService: Injection + let imapSessionProvider: ImapSessionProviderType lazy var logger = Logger.nested(Self.self) - private init( - dataService: Injection = DataService.shared, + init( + user: User, helper: ImapHelperType = ImapHelper(), messageKindProvider: MessageKindProviderType = MessageKindProvider() ) { - self.dataService = dataService + self.user = user + self.imapSessionProvider = ImapSessionProvider(user: user) self.helper = helper self.messageKindProvider = messageKindProvider } diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift b/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift new file mode 100644 index 000000000..c448a6659 --- /dev/null +++ b/FlowCrypt/Functionality/Mail Provider/Imap/ImapSessionProvider.swift @@ -0,0 +1,39 @@ +// +// ImapSessionProvider.swift +// FlowCrypt +// +// Created by Tom on 30.11.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +protocol ImapSessionProviderType { + func imapSession() -> IMAPSession? + func smtpSession() -> SMTPSession? +} + +class ImapSessionProvider: ImapSessionProviderType { + + private let user: User + + init(user: User) { + self.user = user + } + + func imapSession() -> IMAPSession? { + guard let imapSession = IMAPSession(user: self.user) else { + assertionFailure("couldn't create IMAP Session with this parameters") + return nil + } + return imapSession + } + + func smtpSession() -> SMTPSession? { + guard let smtpSession = SMTPSession(user: self.user) else { + assertionFailure("couldn't create SMTP Session with this parameters") + return nil + } + return smtpSession + } +} diff --git a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift index e0fd7f773..bcd789915 100644 --- a/FlowCrypt/Functionality/Mail Provider/MailProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/MailProvider.swift @@ -8,6 +8,7 @@ import Foundation import GoogleAPIClientForREST_Gmail +import UIKit // TODO - Instead of get properties use some DI mechanism // to reuse already initialised services @@ -15,20 +16,15 @@ import GoogleAPIClientForREST_Gmail /// Provides with proper mail services based on current auth type final class MailProvider { - static var shared: MailProvider = MailProvider( - currentAuthType: DataService.shared.currentAuthType, - services: MailServiceProviderFactory.services() - ) - private var currentAuthType: () -> (AuthType?) + private var currentAuthType: AuthType // todo - originally was auto-enclosure, testing + private var authType: AuthType { - switch currentAuthType() { + switch currentAuthType { case let .oAuthGmail(token): return .oAuthGmail(token) case let .password(password): return .password(password) - default: - fatalError("Service can't be resolved. User should be authenticated") } } private let services: [MailServiceProvider] @@ -81,12 +77,12 @@ final class MailProvider { resolveService(of: MessagesThreadOperationsProvider?.self) } - private init( - currentAuthType: @autoclosure @escaping () -> (AuthType?), - services: [MailServiceProvider] + init( + currentAuthType: AuthType, + currentUser: User ) { self.currentAuthType = currentAuthType - self.services = services + self.services = MailServiceProviderFactory.services(user: currentUser) } private func resolveService(of type: T.Type) -> T { @@ -105,10 +101,18 @@ final class MailProvider { } private struct MailServiceProviderFactory { - static func services() -> [MailServiceProvider] { + static func services( + user: User + ) -> [MailServiceProvider] { [ - Imap.shared, - GmailService() + Imap(user: user), + GmailService( + currentUserEmail: user.email, + userService: GoogleUserService( + currentUserEmail: user.email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegateGoogleSesssionContainer + ) + ) ] } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift index f1a99f112..2f52e5d0a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Gmail+Message.swift @@ -10,6 +10,7 @@ import GoogleAPIClientForREST_Gmail import GTMSessionFetcherCore extension GmailService: MessageProvider { + func fetchMsg(message: Message, folder: String, progressHandler: ((MessageFetchState) -> Void)?) async throws -> Data { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift index ab8bad9bc..e3a353bd6 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/Imap+Message.swift @@ -9,6 +9,7 @@ import Foundation extension Imap: MessageProvider { + func fetchMsg( message: Message, folder: String, diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index ec03e4489..36af58f77 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -103,25 +103,25 @@ enum MessageServiceError: Error { final class MessageService { private let messageProvider: MessageProvider - private let keyService: KeyServiceType private let keyMethods: KeyMethodsType - private let passPhraseService: PassPhraseServiceType private let contactsService: ContactsServiceType private let core: Core private let logger: Logger + private let keyService: KeyServiceType + private let passPhraseService: PassPhraseServiceType init( - messageProvider: MessageProvider = MailProvider.shared.messageProvider, - keyService: KeyServiceType = KeyService(), core: Core = Core.shared, - passPhraseService: PassPhraseServiceType = PassPhraseService(), keyMethods: KeyMethodsType = KeyMethods(), - contactsService: ContactsServiceType = ContactsService() + contactsService: ContactsServiceType, + keyService: KeyServiceType, + messageProvider: MessageProvider, + passPhraseService: PassPhraseServiceType ) { - self.messageProvider = messageProvider self.keyService = keyService - self.core = core self.passPhraseService = passPhraseService + self.messageProvider = messageProvider + self.core = core self.logger = Logger.nested(in: Self.self, with: "MessageService") self.keyMethods = keyMethods self.contactsService = contactsService diff --git a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift index cd70685ad..015d2d6e2 100644 --- a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift @@ -10,16 +10,14 @@ import MailCore protocol EnterpriseServerApiType { func getActiveFesUrl(for email: String) async throws -> String? - func getActiveFesUrlForCurrentUser() async throws -> String? - func getClientConfiguration(for email: String) async throws -> RawClientConfiguration - func getClientConfigurationForCurrentUser() async throws -> RawClientConfiguration } enum EnterpriseServerApiError: Error { case parse case emailFormat } + extension EnterpriseServerApiError: LocalizedError { var errorDescription: String? { switch self { @@ -33,6 +31,8 @@ extension EnterpriseServerApiError: LocalizedError { /// https://flowcrypt.com/docs/technical/enterprise/email-deployment-overview.html class EnterpriseServerApi: EnterpriseServerApiType { + static let publicEmailProviderDomains = ["gmail.com", "googlemail.com", "outlook.com"] + private enum Constants { /// 404 - Not Found static let getToleratedHTTPStatuses = [404] @@ -52,17 +52,10 @@ class EnterpriseServerApi: EnterpriseServerApiType { let clientConfiguration: RawClientConfiguration } - func getActiveFesUrlForCurrentUser() async throws -> String? { - guard let email = DataService.shared.currentUser?.email else { - return nil - } - return try await getActiveFesUrl(for: email) - } - func getActiveFesUrl(for email: String) async throws -> String? { do { guard let userDomain = email.recipientDomain, - !Configuration.publicEmailProviderDomains.contains(userDomain) else { + !EnterpriseServerApi.publicEmailProviderDomains.contains(userDomain) else { return nil } let urlString = "https://fes.\(userDomain)/" @@ -104,7 +97,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { return .empty } - if Configuration.publicEmailProviderDomains.contains(userDomain) { + if EnterpriseServerApi.publicEmailProviderDomains.contains(userDomain) { return .empty } let request = ApiCall.Request( @@ -126,10 +119,4 @@ class EnterpriseServerApi: EnterpriseServerApiType { return clientConfiguration } - func getClientConfigurationForCurrentUser() async throws -> RawClientConfiguration { - guard let email = DataService.shared.currentUser?.email else { - fatalError("User has to be set while getting client configuration") - } - return try await getClientConfiguration(for: email) - } } diff --git a/FlowCrypt/Functionality/Services/AppStartup.swift b/FlowCrypt/Functionality/Services/AppStartup.swift index 288c594de..3dba1aa15 100644 --- a/FlowCrypt/Functionality/Services/AppStartup.swift +++ b/FlowCrypt/Functionality/Services/AppStartup.swift @@ -16,9 +16,15 @@ struct AppStartup { case signIn, setupFlow(UserId), mainFlow } + private let appContext: AppContext + + init(appContext: AppContext) { + self.appContext = appContext + } + @MainActor - func initializeApp(window: UIWindow, session: SessionType?) { - logger.logInfo("Initialize application with session \(session.debugDescription)") + func initializeApp(window: UIWindow) { + logger.logInfo("Initialize application with session \(appContext.session.debugDescription)") Task { window.rootViewController = BootstrapViewController() @@ -26,12 +32,12 @@ struct AppStartup { do { await setupCore() - try await DataService.shared.performMigrationIfNeeded() + try await appContext.dataService.performMigrationIfNeeded() try await setupSession() try await getUserOrgRulesIfNeeded() - chooseView(for: window, session: session) + chooseView(for: window) } catch { - showErrorAlert(with: error, on: window, session: session) + showErrorAlert(of: error, on: window) } } } @@ -46,42 +52,42 @@ struct AppStartup { try await renewSessionIfValid() } + /// todo - refactor so that it doesn't need getOptionalMailProvider private func renewSessionIfValid() async throws { - guard DataService.shared.currentAuthType != nil else { + guard let mailProvider = appContext.getOptionalMailProvider() else { return } - return try await MailProvider.shared.sessionProvider.renewSession() + return try await mailProvider.sessionProvider.renewSession() } @MainActor - private func chooseView(for window: UIWindow, session: SessionType?) { - let entryPoint = entryPointForUser(session: session) + private func chooseView(for window: UIWindow) { + let entryPoint = entryPointForUser() let viewController: UIViewController switch entryPoint { case .mainFlow: - let contentViewController = InboxViewContainerController() - viewController = SideMenuNavigationController(contentViewController: contentViewController) + let contentViewController = InboxViewContainerController(appContext: appContext) + viewController = SideMenuNavigationController(appContext: appContext, contentViewController: contentViewController) case .signIn: - viewController = MainNavigationController(rootViewController: SignInViewController()) + viewController = MainNavigationController(rootViewController: SignInViewController(appContext: appContext)) case .setupFlow(let userId): - let setupViewController = SetupInitialViewController(user: userId) + let setupViewController = SetupInitialViewController(appContext: appContext, user: userId) viewController = MainNavigationController(rootViewController: setupViewController) } window.rootViewController = viewController } - private func entryPointForUser(session: SessionType?) -> EntryPoint { - let dataService = DataService.shared - if !dataService.isLoggedIn { + private func entryPointForUser() -> EntryPoint { + if !appContext.dataService.isLoggedIn { logger.logInfo("User is not logged in -> signIn") return .signIn - } else if dataService.isSetupFinished { + } else if appContext.dataService.isSetupFinished { logger.logInfo("Setup finished -> mainFlow") return .mainFlow - } else if let session = session, let userId = makeUserIdForSetup(session: session) { + } else if let session = appContext.session, let userId = makeUserIdForSetup(session: session) { logger.logInfo("User with session \(session) -> setupFlow") return .setupFlow(userId) } else { @@ -91,13 +97,16 @@ struct AppStartup { } private func getUserOrgRulesIfNeeded() async throws { - if DataService.shared.isLoggedIn { - _ = try await ClientConfigurationService().fetchForCurrentUser() + guard let currentUser = appContext.dataService.currentUser else { + return + } + if appContext.dataService.isLoggedIn { + _ = try await appContext.clientConfigurationService.fetch(for: currentUser) } } private func makeUserIdForSetup(session: SessionType) -> UserId? { - guard let currentUser = DataService.shared.currentUser else { + guard let currentUser = appContext.dataService.currentUser else { Logger.logInfo("Can't create user id for setup") return nil } @@ -125,10 +134,10 @@ struct AppStartup { } @MainActor - private func showErrorAlert(with error: Error, on window: UIWindow, session: SessionType?) { + private func showErrorAlert(of error: Error, on window: UIWindow) { let alert = UIAlertController(title: "Startup Error", message: "\(error.localizedDescription)", preferredStyle: .alert) let retry = UIAlertAction(title: "Retry", style: .default) { _ in - self.initializeApp(window: window, session: session) + self.initializeApp(window: window) } alert.addAction(retry) window.rootViewController?.present(alert, animated: true, completion: nil) diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 8b2bdfbac..66b1113bc 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -15,9 +15,9 @@ final class BackupService { let messageSender: MessageGateway init( - backupProvider: BackupProvider = MailProvider.shared.backupProvider, + backupProvider: BackupProvider, core: Core = .shared, - messageSender: MessageGateway = MailProvider.shared.messageSender + messageSender: MessageGateway ) { self.backupProvider = backupProvider self.core = core diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift index 6e563656a..0141079fb 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift @@ -10,51 +10,48 @@ import FlowCryptCommon import Foundation protocol ClientConfigurationServiceType { - func fetchForCurrentUser() async throws -> ClientConfiguration - func getSavedForCurrentUser() -> ClientConfiguration + func fetch(for user: User) async throws -> ClientConfiguration + func getSaved(for user: String) -> ClientConfiguration } final class ClientConfigurationService { private let server: EnterpriseServerApiType private let local: LocalClientConfigurationType - private let getCurrentUserEmail: () -> (String?) init( server: EnterpriseServerApiType = EnterpriseServerApi(), - local: LocalClientConfigurationType = LocalClientConfiguration(), - getCurrentUserEmail: @autoclosure @escaping () -> (String?) = DataService.shared.currentUser?.email + local: LocalClientConfigurationType ) { self.server = server self.local = local - self.getCurrentUserEmail = getCurrentUserEmail } } // MARK: - OrganisationalRulesServiceType extension ClientConfigurationService: ClientConfigurationServiceType { - func fetchForCurrentUser() async throws -> ClientConfiguration { - guard let currentUserEmail = getCurrentUserEmail() else { - throw AppErr.noCurrentUser - } + func fetch(for user: User) async throws -> ClientConfiguration { +// guard let user = user else { +// throw AppErr.noCurrentUser +// } do { - let raw = try await server.getClientConfiguration(for: currentUserEmail) - local.save(raw: raw) + let raw = try await server.getClientConfiguration(for: user.email) + local.save(for: user, raw: raw) return ClientConfiguration(raw: raw) } catch { - guard let raw = local.load() else { + guard let raw = local.load(for: user.email) else { throw error } return ClientConfiguration(raw: raw) } } - func getSavedForCurrentUser() -> ClientConfiguration { - guard let raw = self.local.load() else { + func getSaved(for userEmail: String) -> ClientConfiguration { + guard let raw = self.local.load(for: userEmail) else { + // todo - throw instead fatalError("There should not be a user without OrganisationalRules") } - return ClientConfiguration(raw: raw) } } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift index 9e2472c3a..f8dae16b7 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift @@ -11,36 +11,29 @@ import RealmSwift import IDZSwiftCommonCrypto protocol LocalClientConfigurationType { - func load() -> RawClientConfiguration? - func remove() - func save(raw: RawClientConfiguration) + func load(for user: String) -> RawClientConfiguration? + func remove(for user: String) + func save(for user: User, raw: RawClientConfiguration) } struct LocalClientConfiguration { let cache: EncryptedCacheService - init(encryptedStorage: EncryptedStorageType = EncryptedStorage()) { + init(encryptedStorage: EncryptedStorageType) { self.cache = EncryptedCacheService(encryptedStorage: encryptedStorage) } } extension LocalClientConfiguration: LocalClientConfigurationType { - func load() -> RawClientConfiguration? { - // (tom) todo - should we not guard here? -// guard let user = cache.encryptedStorage.activeUser else { -// fatalError("Internal inconsistency, no active user when loading client configuration") -// } - RawClientConfiguration(cache.getAllForActiveUser()?.first) + func load(for userEmail: String) -> RawClientConfiguration? { + guard let foundLocal = cache.getAll(for: userEmail).first else { return nil } + return RawClientConfiguration(foundLocal) } - func remove() { - // (tom) todo - should we not guard here? - cache.removeAllForActiveUser() + func remove(for userEmail: String) { + cache.removeAll(for: userEmail) } - func save(raw: RawClientConfiguration) { - guard let user = cache.encryptedStorage.activeUser else { - fatalError("Internal inconsistency, no active user when saving client configuration") - } + func save(for user: User, raw: RawClientConfiguration) { cache.save(ClientConfigurationRealmObject(configuration: raw, user: user)) } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index c01082c35..cc136d9b5 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -12,6 +12,8 @@ import Foundation import GoogleAPIClientForREST_Gmail import FlowCryptCommon +typealias RecipientState = RecipientEmailsCellNode.Input.State + struct ComposeMessageContext: Equatable { var message: String? var recipients: [ComposeMessageRecipient] = [] @@ -33,24 +35,30 @@ protocol CoreComposeMessageType { } final class ComposeMessageService { + private let messageGateway: MessageGateway - private let dataService: KeyStorageType + private let keyStorage: KeyStorageType private let contactsService: ContactsServiceType private let core: CoreComposeMessageType & KeyParser private let draftGateway: DraftGateway? private let logger: Logger init( - messageGateway: MessageGateway = MailProvider.shared.messageSender, - draftGateway: DraftGateway? = MailProvider.shared.draftGateway, - dataService: KeyStorageType = KeyDataStorage(), - contactsService: ContactsServiceType = ContactsService(), + clientConfiguration: ClientConfiguration, + encryptedStorage: EncryptedStorageType, + messageGateway: MessageGateway, + draftGateway: DraftGateway? = nil, + keyStorage: KeyStorageType? = nil, + contactsService: ContactsServiceType? = nil, core: CoreComposeMessageType & KeyParser = Core.shared ) { self.messageGateway = messageGateway self.draftGateway = draftGateway - self.dataService = dataService - self.contactsService = contactsService + self.keyStorage = keyStorage ?? KeyDataStorage(encryptedStorage: encryptedStorage) + self.contactsService = contactsService ?? ContactsService( + localContactsProvider: LocalContactsProvider(encryptedStorage: encryptedStorage), + clientConfiguration: clientConfiguration + ) self.core = core self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService") } @@ -88,7 +96,7 @@ final class ComposeMessageService { let subject = contextToSend.subject ?? "(no subject)" - guard let myPubKey = self.dataService.publicKey() else { + guard let myPubKey = self.keyStorage.publicKey() else { throw MessageValidationError.missedPublicKey } diff --git a/FlowCrypt/Functionality/Services/EncryptedCacheService.swift b/FlowCrypt/Functionality/Services/EncryptedCacheService.swift index 460f635e2..38451ddda 100644 --- a/FlowCrypt/Functionality/Services/EncryptedCacheService.swift +++ b/FlowCrypt/Functionality/Services/EncryptedCacheService.swift @@ -26,6 +26,7 @@ final class EncryptedCacheService { } func save(_ object: T) { + // todo - should throw instead, don't "try?" try? realm.write { realm.add(object, update: .modified) } @@ -37,28 +38,26 @@ final class EncryptedCacheService { .first(where: { $0.identifier == identifier }) else { return } + // todo - should throw instead, don't "try?" try? realm.write { realm.delete(objectToDelete) } } func remove(objects: [T]) { + // todo - should throw instead, don't "try?" try? realm.write { realm.delete(objects) } } - func removeAllForActiveUser() { - let allObjects = getAllForActiveUser() ?? [] + func removeAll(for userEmail: String) { + let allObjects = getAll(for: userEmail) remove(objects: allObjects) } - func getAllForActiveUser() -> [T]? { - let currentUser = realm - .objects(UserRealmObject.self) - .first(where: \.isActive) - + func getAll(for userEmail: String) -> [T] { return Array(realm.objects(T.self)) - .filter { $0.activeUser?.email == currentUser?.email } + .filter { $0.activeUser?.email == userEmail } } } diff --git a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift index 18fc16c8c..03ce97e65 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift @@ -14,7 +14,7 @@ protocol TrashFolderProviderType { } protocol FoldersServiceType { - func fetchFolders(isForceReload: Bool) async throws -> [FolderViewModel] + func fetchFolders(isForceReload: Bool, for user: User) async throws -> [FolderViewModel] } final class FoldersService: FoldersServiceType { @@ -24,40 +24,41 @@ final class FoldersService: FoldersServiceType { private let remoteFoldersProvider: RemoteFoldersProviderType init( - localFoldersProvider: LocalFoldersProviderType = LocalFoldersProvider(), - remoteFoldersProvider: RemoteFoldersProviderType = MailProvider.shared.remoteFoldersProvider, + encryptedStorage: EncryptedStorageType, + localFoldersProvider: LocalFoldersProviderType? = nil, + remoteFoldersProvider: RemoteFoldersProviderType, trashPathStorage: LocalStorageType = LocalStorage() ) { - self.localFoldersProvider = localFoldersProvider + self.localFoldersProvider = localFoldersProvider ?? LocalFoldersProvider(encryptedStorage: encryptedStorage) self.remoteFoldersProvider = remoteFoldersProvider self.trashPathStorage = trashPathStorage } - func fetchFolders(isForceReload: Bool) async throws -> [FolderViewModel] { + func fetchFolders(isForceReload: Bool, for user: User) async throws -> [FolderViewModel] { if isForceReload { - return try await getAndSaveFolders() + return try await getAndSaveFolders(for: user) } - let localFolders = self.localFoldersProvider.fetchFolders() + let localFolders = self.localFoldersProvider.fetchFolders(for: user.email) if localFolders.isEmpty { - return try await getAndSaveFolders() + return try await getAndSaveFolders(for: user) } else { - try await getAndSaveFolders() + try await getAndSaveFolders(for: user) return localFolders } } @discardableResult - private func getAndSaveFolders() async throws -> [FolderViewModel] { + private func getAndSaveFolders(for user: User) async throws -> [FolderViewModel] { // fetch all folders let fetchedFolders = try await self.remoteFoldersProvider.fetchFolders() return try await withCheckedThrowingContinuation { continuation in DispatchQueue.main.async { // TODO: - Ticket? - instead of removing all folders remove only // those folders which are in DB and not in remoteFolders - self.localFoldersProvider.removeFolders() + self.localFoldersProvider.removeFolders(for: user.email) // save to Realm - self.localFoldersProvider.save(folders: fetchedFolders) + self.localFoldersProvider.save(folders: fetchedFolders, for: user) // save trash folder path self.saveTrashFolderPath(with: fetchedFolders.map(\.path)) diff --git a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift index 705f1b47d..b24264c29 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift @@ -10,34 +10,28 @@ import Foundation import RealmSwift protocol LocalFoldersProviderType { - func fetchFolders() -> [FolderViewModel] - func removeFolders() - func save(folders: [Folder]) + func fetchFolders(for userEmail: String) -> [FolderViewModel] + func removeFolders(for userEmail: String) + func save(folders: [Folder], for user: User) } struct LocalFoldersProvider: LocalFoldersProviderType { private let folderCache: EncryptedCacheService - init(encryptedStorage: EncryptedStorageType = EncryptedStorage()) { + init(encryptedStorage: EncryptedStorageType) { self.folderCache = EncryptedCacheService(encryptedStorage: encryptedStorage) } - func fetchFolders() -> [FolderViewModel] { - folderCache.getAllForActiveUser()? - .compactMap(FolderViewModel.init) - ?? [] + func fetchFolders(for userEmail: String) -> [FolderViewModel] { + return folderCache.getAll(for: userEmail).compactMap(FolderViewModel.init) } - func save(folders: [Folder]) { - guard let currentUser = folderCache.encryptedStorage.activeUser else { - return - } - - folders.map { FolderRealmObject(folder: $0, user: currentUser) } + func save(folders: [Folder], for user: User) { + folders.map { FolderRealmObject(folder: $0, user: user) } .forEach(folderCache.save) } - func removeFolders() { - folderCache.removeAllForActiveUser() + func removeFolders(for userEmail: String) { + folderCache.removeAll(for: userEmail) } } diff --git a/FlowCrypt/Functionality/Services/Folders Services/TrashFolderProvider.swift b/FlowCrypt/Functionality/Services/Folders Services/TrashFolderProvider.swift index e36818693..8608fdc88 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/TrashFolderProvider.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/TrashFolderProvider.swift @@ -9,13 +9,17 @@ struct TrashFolderProvider { private let localStorage: LocalStorageType private let folderProvider: FoldersServiceType + private let user: User init( - folderProvider: FoldersServiceType = FoldersService(), + // todo - rename argument to folderService: + user: User, + folderProvider: FoldersServiceType, localStorage: LocalStorageType = LocalStorage() ) { self.folderProvider = folderProvider self.localStorage = localStorage + self.user = user } } @@ -24,7 +28,7 @@ extension TrashFolderProvider: TrashFolderProviderType { if let path = localStorage.trashFolderPath { return path } else { - _ = try await folderProvider.fetchFolders(isForceReload: true) + _ = try await folderProvider.fetchFolders(isForceReload: true, for: self.user) return localStorage.trashFolderPath } } diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 1e7b250f4..0419095a5 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -12,10 +12,10 @@ import UIKit @MainActor protocol GlobalRouterType { func proceed() - func signIn(with route: GlobalRoutingType) - func askForContactsPermission(for route: GlobalRoutingType) async throws - func switchActive(user: User) - func signOut() + func signIn(appContext: AppContext, route: GlobalRoutingType) + func askForContactsPermission(for route: GlobalRoutingType, appContext: AppContext) async throws + func switchActive(user: User, appContext: AppContext) + func signOut(appContext: AppContext) } enum GlobalRoutingType { @@ -36,75 +36,82 @@ final class GlobalRouter { return delegate.window } - private let userAccountService: UserAccountServiceType - private let googleService: GoogleUserService - private lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) - init( - userAccountService: UserAccountServiceType = UserAccountService(), - googleService: GoogleUserService = GoogleUserService() - ) { - self.userAccountService = userAccountService - self.googleService = googleService - } } // MARK: - Proceed extension GlobalRouter: GlobalRouterType { + /// proceed to flow (signing/setup/app) depends on user status (isLoggedIn/isSetupFinished) func proceed() { - validateEncryptedStorage { - userAccountService.cleanupSessions() - proceed(with: nil) + do { + let appContext = try AppContext.setUpAppContext(globalRouter: self) + do { + try appContext.encryptedStorage.validate() + proceed(with: appContext) + } catch { + renderInvalidStorageView(error: error, encryptedStorage: nil) + } + } catch { + renderInvalidStorageView(error: error, encryptedStorage: nil) } } - func signIn(with route: GlobalRoutingType) { + func signIn(appContext: AppContext, route: GlobalRoutingType) { logger.logInfo("Sign in with \(route)") switch route { case .gmailLogin(let viewController): Task { do { + let googleService = GoogleUserService( + currentUserEmail: appContext.dataService.currentUser?.email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate + ) let session = try await googleService.signIn( in: viewController, scopes: GeneralConstants.Gmail.mailScope ) - self.userAccountService.startSessionFor(user: session) - self.proceed(with: session) + appContext.userAccountService.startSessionFor(session: session) + self.proceed(with: appContext.withSession(session)) } catch { - self.handleGmailError(error, in: viewController) + self.handleGmailError(appContext: appContext, error, in: viewController) } } case .other(let session): - userAccountService.startSessionFor(user: session) - proceed(with: session) + appContext.userAccountService.startSessionFor(session: session) + proceed(with: appContext.withSession(session)) } } - func signOut() { - if let session = userAccountService.startActiveSessionForNextUser() { + func signOut(appContext: AppContext) { + if let session = appContext.userAccountService.startActiveSessionForNextUser() { logger.logInfo("Start session for another email user \(session)") - proceed(with: session) + proceed(with: appContext.withSession(session)) } else { logger.logInfo("Sign out") - userAccountService.cleanup() + appContext.userAccountService.cleanup() proceed() } } - func askForContactsPermission(for route: GlobalRoutingType) async throws { + func askForContactsPermission(for route: GlobalRoutingType, appContext: AppContext) async throws { logger.logInfo("Ask for contacts permission with \(route)") switch route { case .gmailLogin(let viewController): do { + let googleService = GoogleUserService( + currentUserEmail: appContext.dataService.currentUser?.email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate + ) let session = try await googleService.signIn( in: viewController, scopes: GeneralConstants.Gmail.contactsScope ) - self.userAccountService.startSessionFor(user: session) + appContext.userAccountService.startSessionFor(session: session) + // todo? - no need to update context itself with new session? } catch { logger.logInfo("Contacts scope failed with error \(error.errorMessage)") throw error @@ -114,45 +121,40 @@ extension GlobalRouter: GlobalRouterType { } } - func switchActive(user: User) { + func switchActive(user: User, appContext: AppContext) { logger.logInfo("Switching active user \(user)") - guard let session = userAccountService.switchActiveSessionFor(user: user) else { + guard let session = appContext.userAccountService.switchActiveSessionFor(user: user) else { logger.logWarning("Can't switch active user with \(user.email)") return } - proceed(with: session) + proceed(with: appContext.withSession(session)) } @MainActor - private func validateEncryptedStorage(_ completion: () -> Void) { - let storage = EncryptedStorage() - do { - try storage.validate() - completion() - } catch { - let controller = InvalidStorageViewController( - error: error, - encryptedStorage: storage, - router: self - ) - keyWindow.rootViewController = UINavigationController(rootViewController: controller) - keyWindow.makeKeyAndVisible() - } + private func renderInvalidStorageView(error: Error, encryptedStorage: EncryptedStorageType?) { + // EncryptedStorage is nil if we could not successfully initialize it + let controller = InvalidStorageViewController( + error: error, + encryptedStorage: encryptedStorage, + router: self + ) + keyWindow.rootViewController = UINavigationController(rootViewController: controller) + keyWindow.makeKeyAndVisible() } @MainActor - private func proceed(with session: SessionType?) { - logger.logInfo("proceed for session \(session.debugDescription)") - AppStartup().initializeApp(window: keyWindow, session: session) + private func proceed(with appContext: AppContext) { + logger.logInfo("proceed for session: \(appContext.session?.description ?? "nil")") + AppStartup(appContext: appContext).initializeApp(window: keyWindow) } @MainActor - private func handleGmailError(_ error: Error, in viewController: UIViewController) { + private func handleGmailError(appContext: AppContext, _ error: Error, in viewController: UIViewController) { logger.logInfo("gmail login failed with error \(error.errorMessage)") if let gmailUserError = error as? GoogleUserServiceError, case .userNotAllowedAllNeededScopes = gmailUserError { let navigationController = viewController.navigationController - let checkAuthViewController = CheckMailAuthViewController() + let checkAuthViewController = CheckMailAuthViewController(appContext: appContext) navigationController?.pushViewController(checkAuthViewController, animated: true) } } diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index 7c444f4c4..72a93e937 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -13,7 +13,6 @@ import GTMAppAuth import RealmSwift protocol UserServiceType { - func signOut(user email: String) func signIn(in viewController: UIViewController, scopes: [GoogleScope]) async throws -> SessionType func renewSession() async throws } @@ -48,12 +47,30 @@ protocol GoogleUserServiceType { func renewSession() async throws } +// this is here so that we don't have to include AppDelegate in test target +protocol AppDelegateGoogleSesssionContainer { + var googleAuthSession: OIDExternalUserAgentSession? { get set } +} + +// todo - should be refactored to not require currentUserEmail final class GoogleUserService: NSObject, GoogleUserServiceType { + let currentUserEmail: String? + var appDelegateGoogleSessionContainer: AppDelegateGoogleSesssionContainer? + + init( + currentUserEmail: String?, + appDelegateGoogleSessionContainer: AppDelegateGoogleSesssionContainer? = nil + ) { + self.appDelegateGoogleSessionContainer = appDelegateGoogleSessionContainer + self.currentUserEmail = currentUserEmail + } + private enum Constants { static let index = "GTMAppAuthAuthorizerIndex" static let userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo" } + private lazy var logger = Logger.nested(in: Self.self, with: .userAppStart) var userToken: String? { @@ -72,15 +89,9 @@ final class GoogleUserService: NSObject, GoogleUserServiceType { getAuthorizationForCurrentUser() } - private var currentUserEmail: String? { - DataService.shared.email - } } extension GoogleUserService: UserServiceType { - private var appDelegate: AppDelegate? { - UIApplication.shared.delegate as? AppDelegate - } func renewSession() async throws { // GTMAppAuth should renew session via OIDAuthStateChangeDelegate @@ -89,12 +100,11 @@ extension GoogleUserService: UserServiceType { @MainActor func signIn(in viewController: UIViewController, scopes: [GoogleScope]) async throws -> SessionType { return try await withCheckedThrowingContinuation { continuation in let request = self.makeAuthorizationRequest(scopes: scopes) - let googleAuthSession = OIDAuthState.authState( + let googleDelegateSess = OIDAuthState.authState( byPresenting: request, presenting: viewController ) { [weak self] authState, authError in guard let self = self else { return } - guard let authState = authState else { if let authError = authError { let error = self.parseSignInError(authError) @@ -104,7 +114,6 @@ extension GoogleUserService: UserServiceType { return continuation.resume(throwing: error) } } - Task { do { return continuation.resume(returning: try await self.handleGoogleAuthStateResult(authState, scopes: scopes)) @@ -113,13 +122,13 @@ extension GoogleUserService: UserServiceType { } } } - self.appDelegate?.googleAuthSession = googleAuthSession + self.appDelegateGoogleSessionContainer?.googleAuthSession = googleDelegateSess } } func signOut(user email: String) { DispatchQueue.main.async { - self.appDelegate?.googleAuthSession = nil + self.appDelegateGoogleSessionContainer?.googleAuthSession = nil GTMAppAuthFetcherAuthorization.removeFromKeychain(forName: Constants.index + email) } } @@ -202,7 +211,7 @@ extension GoogleUserService { return try JSONDecoder().decode(GoogleUser.self, from: data) } catch { let isTokenErr = (error as NSError).isEqual(OIDOAuthTokenErrorDomain) - if isTokenErr, let email = self.currentUserEmail { + if isTokenErr, let email = currentUserEmail { self.logger.logError("Authorization error during token refresh, clearing state. \(error)") // removes any authorisation information which was stored in Keychain, the same happens on logout. // if any error happens during token refresh then user will be signed out automatically. @@ -226,7 +235,6 @@ extension GoogleUserService: OIDAuthStateChangeDelegate { guard let email = currentUserEmail else { return } - saveAuth(state: state, for: email) } } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyDataStorage.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyDataStorage.swift index 64affeee4..2466a27d4 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyDataStorage.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyDataStorage.swift @@ -9,13 +9,12 @@ import FlowCryptCommon import Foundation +// todo - what is this and what is it used for? final class KeyDataStorage { private let encryptedStorage: EncryptedStorageType - init( - encryptedStorage: EncryptedStorageType = EncryptedStorage() - ) { + init(encryptedStorage: EncryptedStorageType) { self.encryptedStorage = encryptedStorage } } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift index 598416b8e..2910d0579 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift @@ -28,9 +28,9 @@ final class KeyService: KeyServiceType { let logger: Logger init( - storage: KeyStorageType = KeyDataStorage(), - passPhraseService: PassPhraseServiceType = PassPhraseService(), - currentUserEmail: @autoclosure @escaping () -> (String?) = DataService.shared.email + storage: KeyStorageType, + passPhraseService: PassPhraseServiceType, + currentUserEmail: @escaping () -> (String?) ) { self.storage = storage self.passPhraseService = passPhraseService diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift index 7fecbb17a..3ba7dd543 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift @@ -72,18 +72,15 @@ protocol PassPhraseServiceType { final class PassPhraseService: PassPhraseServiceType { private lazy var logger = Logger.nested(Self.self) - let currentUserEmail: String? let encryptedStorage: PassPhraseStorageType let inMemoryStorage: PassPhraseStorageType init( - encryptedStorage: PassPhraseStorageType = EncryptedStorage(), - localStorage: PassPhraseStorageType = InMemoryPassPhraseStorage(), - emailProvider: EmailProviderType = DataService.shared + encryptedStorage: PassPhraseStorageType, + localStorage: PassPhraseStorageType = InMemoryPassPhraseStorage() ) { self.encryptedStorage = encryptedStorage self.inMemoryStorage = localStorage - self.currentUserEmail = emailProvider.email } func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) { diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift index 0a9b6045a..808a0342b 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift @@ -30,14 +30,14 @@ protocol PublicKeyProvider { struct ContactsService: ContactsServiceType { let localContactsProvider: LocalContactsProviderType - let pubLookup: PubLookupType + let pubLookup: PubLookup init( - localContactsProvider: LocalContactsProviderType = LocalContactsProvider(), - pubLookup: PubLookupType = PubLookup() + localContactsProvider: LocalContactsProviderType, + clientConfiguration: ClientConfiguration ) { self.localContactsProvider = localContactsProvider - self.pubLookup = pubLookup + self.pubLookup = PubLookup(clientConfiguration: clientConfiguration) } } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 52379e5e9..27601648f 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -24,7 +24,7 @@ struct LocalContactsProvider { let core: Core init( - encryptedStorage: EncryptedStorageType = EncryptedStorage(), + encryptedStorage: EncryptedStorageType, core: Core = .shared ) { self.localContactsCache = EncryptedCacheService(encryptedStorage: encryptedStorage) diff --git a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift index 9d736a865..95734a5f8 100644 --- a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift @@ -9,7 +9,7 @@ import Foundation protocol EmailKeyManagerApiType { - func getPrivateKeys() async throws -> EmailKeyManagerApiResult + func getPrivateKeys(currentUserEmail: String) async throws -> EmailKeyManagerApiResult } enum EmailKeyManagerApiError: Error { @@ -40,23 +40,26 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { static let apiName = "EmailKeyManagerApi" } - private let clientConfigurationService: ClientConfigurationServiceType + private let clientConfiguration: ClientConfiguration private let core: Core init( - clientConfigurationService: ClientConfigurationServiceType = ClientConfigurationService(), + clientConfiguration: ClientConfiguration, core: Core = .shared ) { - self.clientConfigurationService = clientConfigurationService + self.clientConfiguration = clientConfiguration self.core = core } - func getPrivateKeys() async throws -> EmailKeyManagerApiResult { + func getPrivateKeys(currentUserEmail: String) async throws -> EmailKeyManagerApiResult { guard let urlString = getPrivateKeysUrlString() else { throw EmailKeyManagerApiError.noPrivateKeysUrlString } - guard let idToken = GoogleUserService().idToken else { + guard let idToken = GoogleUserService( + currentUserEmail: currentUserEmail, + appDelegateGoogleSessionContainer: nil // only needed when signing in/out + ).idToken else { throw EmailKeyManagerApiError.noGoogleIdToken } @@ -95,7 +98,7 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { } private func getPrivateKeysUrlString() -> String? { - guard let keyManagerUrlString = clientConfigurationService.getSavedForCurrentUser().keyManagerUrlString else { + guard let keyManagerUrlString = clientConfiguration.keyManagerUrlString else { return nil } return "\(keyManagerUrlString)v1/keys/private" diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift index f004add8d..6e4349cd1 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift @@ -26,10 +26,10 @@ final class AttesterApi: AttesterApiType { init( core: Core = .shared, - clientConfigurationService: ClientConfigurationServiceType = ClientConfigurationService() + clientConfiguration: ClientConfiguration ) { self.core = core - self.clientConfiguration = clientConfigurationService.getSavedForCurrentUser() + self.clientConfiguration = clientConfiguration } private func urlPub(emailOrLongid: String) -> String { diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/PubLookup.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/PubLookup.swift index b84cc517d..5acea53f8 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/PubLookup.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/PubLookup.swift @@ -25,11 +25,12 @@ class PubLookup: PubLookupType { } init( + clientConfiguration: ClientConfiguration, wkd: WkdApiType = WkdApi(), - attesterApi: AttesterApiType = AttesterApi() + attesterApi: AttesterApiType? = nil ) { self.wkd = wkd - self.attesterApi = attesterApi + self.attesterApi = attesterApi ?? AttesterApi(clientConfiguration: clientConfiguration) } func lookup(email: String) async throws -> RecipientWithSortedPubKeys { diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift index 05f32d7ed..b228c22f7 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift @@ -42,7 +42,7 @@ class WkdApi: WkdApiType { func lookup(email: String) async throws -> [KeyDetails] { guard - !Configuration.publicEmailProviderDomains.contains(email.recipientDomain ?? ""), + !EnterpriseServerApi.publicEmailProviderDomains.contains(email.recipientDomain ?? ""), let advancedUrl = urlConstructor.construct(from: email, method: .advanced), let directUrl = urlConstructor.construct(from: email, method: .direct) else { diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index e783ad01e..2de765c50 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -105,7 +105,7 @@ "setup_enter" = "Enter your pass phrase"; "setup_load" = "Load Account"; "setup_use_another" = "Use Another Account"; -"setup_no_backups" = "No backups found on account: \n"; +"setup_no_backups" = "No backups found on this account"; "setup_enter_pass_phrase" = "Enter pass phrase"; "setup_wrong_pass_phrase_retry" = "Wrong pass phrase, please try again"; "setup_backup_email" = "This email contains a key backup. It will help you access your encrypted messages from other computers (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nThe key below is protected with pass phrase that only you know. You should make sure to note your pass phrase down.\n\nDO NOT DELETE THIS EMAIL. Write us at human@flowcrypt.com so that we can help."; @@ -268,4 +268,4 @@ "invalid_storage_text" = "Your data storage is invalid. You could reset it and start with new one. Check error description below"; "invalid_storage_reset_button" = "Reset"; "invalid_storage_reset_error" = "Couldn't remove storage. Please reinstall application"; - +"invalid_storage_failed_to_initialize" = "Failed to initialize storage. Please restart your device. If it doesn't help, please reinstall application"; diff --git a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift index 5bf801d77..491a79f37 100644 --- a/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift +++ b/FlowCryptAppTests/Functionality/Mail Provider/GmailServiceTest.swift @@ -19,7 +19,11 @@ class GmailServiceTest: XCTestCase { override func setUp() { userService = GoogleUserServiceMock() backupSearchQueryProvider = GmailBackupSearchQueryProviderMock() - sut = GmailService(userService: userService, backupSearchQueryProvider: backupSearchQueryProvider) + sut = GmailService( + currentUserEmail: "user@example.test", + userService: userService, + backupSearchQueryProvider: backupSearchQueryProvider + ) } func testSearchBackupsWhenErrorInQuery() async { diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationServiceTests.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationServiceTests.swift index 910d93f92..63eeee103 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/ClientConfigurationServiceTests.swift @@ -15,6 +15,7 @@ final class ClientConfigurationServiceTests: XCTestCase { var enterpriseServerApi: EnterpriseServerApiMock! var localClientConfigurationProvider: LocalClientConfigurationMock! var isCurrentUserExistMock: CurrentUserEmailMock! + let user = User(email: "example@flowcrypt.test", isActive: true, name: "User", imap: nil, smtp: nil) override func setUp() { super.setUp() @@ -24,8 +25,7 @@ final class ClientConfigurationServiceTests: XCTestCase { sut = ClientConfigurationService( server: enterpriseServerApi, - local: localClientConfigurationProvider, - getCurrentUserEmail: self.isCurrentUserExistMock.currentUserEmail() + local: localClientConfigurationProvider ) } @@ -35,7 +35,7 @@ final class ClientConfigurationServiceTests: XCTestCase { expectedConfiguration } - let clientConfiguration = sut.getSavedForCurrentUser() + let clientConfiguration = sut.getSaved(for: user.email) XCTAssert(localClientConfigurationProvider.fetchCount == 1) XCTAssert(localClientConfigurationProvider.fetchInvoked == true) XCTAssert(clientConfiguration.raw == expectedConfiguration) @@ -46,7 +46,7 @@ final class ClientConfigurationServiceTests: XCTestCase { nil } do { - _ = try await sut.fetchForCurrentUser() + _ = try await sut.fetch(for: user) XCTFail() } catch { } @@ -68,7 +68,7 @@ final class ClientConfigurationServiceTests: XCTestCase { "example@flowcrypt.test" } - _ = try await sut.fetchForCurrentUser() + _ = try await sut.fetch(for: user) } func testInCaseGetClientConfigurationReturnsError() async throws { @@ -86,7 +86,7 @@ final class ClientConfigurationServiceTests: XCTestCase { expectedClientConfiguration } - let clientConfiguration = try await sut.fetchForCurrentUser() + let clientConfiguration = try await sut.fetch(for: user) XCTAssertTrue(clientConfiguration.raw == expectedClientConfiguration) } } diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift index 24123244b..6234f866c 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/ClientConfigurationProviderMock.swift @@ -10,12 +10,13 @@ import Foundation class LocalClientConfigurationMock: LocalClientConfigurationType { + var fetchInvoked = false var fetchCount = 0 var fetchCall: () -> (RawClientConfiguration?) = { nil } - func load() -> RawClientConfiguration? { + func load(for user: String) -> RawClientConfiguration? { fetchInvoked = true fetchCount += 1 return fetchCall() @@ -23,7 +24,7 @@ class LocalClientConfigurationMock: LocalClientConfigurationType { var removeClientConfigurationInvoked = false var removeClientConfigurationCount = 0 - func remove() { + func remove(for user: String) { removeClientConfigurationInvoked = true removeClientConfigurationCount += 1 } @@ -32,7 +33,7 @@ class LocalClientConfigurationMock: LocalClientConfigurationType { var saveCount = 0 var saveCall: (RawClientConfiguration) -> Void = { clientConfiguration in } - func save(raw: RawClientConfiguration) { + func save(for user: User, raw: RawClientConfiguration) { saveInvoked = true saveCount += 1 saveCall(raw) diff --git a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift index 9cf5e8176..e4785fac3 100644 --- a/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift +++ b/FlowCryptAppTests/Functionality/Services/Client Configuration Service/Mocks/OrganisationalRulesServiceMock.swift @@ -21,11 +21,8 @@ final class OrganisationalRulesServiceMock: ClientConfigurationServiceType { } } - var fetchOrganisationalRulesForEmail: (String) throws -> ClientConfiguration = { _ in - throw MockError() - } - func fetchOrganisationalRules(for email: String) async throws -> ClientConfiguration { - return try fetchOrganisationalRulesForEmail(email) + func fetch(for user: User) async throws -> ClientConfiguration { + throw MockError() // ?? } var clientConfiguration: RawClientConfiguration! @@ -33,7 +30,7 @@ final class OrganisationalRulesServiceMock: ClientConfigurationServiceType { var getSavedOrganisationalRulesForCurrentUserResult: ClientConfiguration { ClientConfiguration(raw: clientConfiguration) } - func getSavedForCurrentUser() -> ClientConfiguration { + func getSaved(for user: String) -> ClientConfiguration { getSavedOrganisationalRulesForCurrentUserResult } } diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index fd546653e..3ba8d0abf 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -28,11 +28,17 @@ class ComposeMessageServiceTests: XCTestCase { override func setUp() { super.setUp() - + let storageEncryptionKey = CoreHost().getSecureRandomByteNumberArray(64)! sut = ComposeMessageService( + clientConfiguration: ClientConfiguration( + raw: RawClientConfiguration() + ), + encryptedStorage: EncryptedStorage( + storageEncryptionKey: Data(storageEncryptionKey) + ), messageGateway: MessageGatewayMock(), draftGateway: DraftGatewayMock(), - dataService: keyStorage, + keyStorage: keyStorage, contactsService: contactsService, core: core ) diff --git a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift index 71cca6104..42f5d37e3 100644 --- a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift @@ -54,7 +54,7 @@ final class KeyServiceTests: XCTestCase { let keyService = KeyService( storage: keyStorage, passPhraseService: passPhraseService, - currentUserEmail: "bill@test.com" + currentUserEmail: { "bill@test.com" } ) // act @@ -83,7 +83,7 @@ final class KeyServiceTests: XCTestCase { let keyService = KeyService( storage: keyStorage, passPhraseService: PassPhraseServiceMock(), - currentUserEmail: "bill@test.com" + currentUserEmail: { "bill@test.com" } ) // act diff --git a/FlowCryptAppTests/Functionality/Services/Key Services/PubLookupTest.swift b/FlowCryptAppTests/Functionality/Services/Key Services/PubLookupTest.swift index 31c7dc5da..55375cec0 100644 --- a/FlowCryptAppTests/Functionality/Services/Key Services/PubLookupTest.swift +++ b/FlowCryptAppTests/Functionality/Services/Key Services/PubLookupTest.swift @@ -15,7 +15,12 @@ class PubLookupTest: XCTestCase { // tests https://github.com/FlowCrypt/flowcrypt-ios/issues/809 // fetches https://flowcrypt.com/attester/pub/different.uid@recipient.test // if this test starts failing, ensure the right pubkey is still on prod Attester - let r = try await PubLookup().lookup(email: "different.uid@recipient.test") + let pubLookup = PubLookup( + clientConfiguration: ClientConfiguration( + raw: RawClientConfiguration() + ) + ) + let r = try await pubLookup.lookup(email: "different.uid@recipient.test") XCTAssertTrue(r.pubKeys.isNotEmpty, "expected pubkeys not empty") XCTAssertEqual(r.pubKeys.first?.longid, "0C9C2E6A4D273C6F") } diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/EmailProviderMock.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/EmailProviderMock.swift deleted file mode 100644 index d9076ec43..000000000 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/EmailProviderMock.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// EmailProviderMock.swift -// FlowCryptTests -// -// Created by Anton Kharchevskyi on 07.06.2021. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -@testable import FlowCrypt -import Foundation - -class EmailProviderMock: EmailProviderType { - var email: String? = "test@gmail.com" -} diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift index e1ad0ab20..c2def833a 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift @@ -14,17 +14,14 @@ class PassPhraseStorageTests: XCTestCase { var sut: PassPhraseService! var encryptedStorage: PassPhraseStorageMock! var inMemoryStorage: PassPhraseStorageMock! - var emailProvider: EmailProviderMock! override func setUp() { - emailProvider = EmailProviderMock() encryptedStorage = PassPhraseStorageMock() inMemoryStorage = PassPhraseStorageMock() sut = PassPhraseService( encryptedStorage: encryptedStorage, - localStorage: inMemoryStorage, - emailProvider: emailProvider + localStorage: inMemoryStorage ) } diff --git a/FlowCryptUI/Nodes/TableViewController.swift b/FlowCryptUI/Nodes/TableViewController.swift index d6506df1e..2c5143067 100644 --- a/FlowCryptUI/Nodes/TableViewController.swift +++ b/FlowCryptUI/Nodes/TableViewController.swift @@ -10,6 +10,7 @@ import AsyncDisplayKit @MainActor open class TableNodeViewController: ASDKViewController { + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) node.reloadData() diff --git a/Podfile.lock b/Podfile.lock index d4c396aa3..95812844b 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,8 +15,8 @@ PODS: - PINRemoteImage/PINCache (3.0.3): - PINCache (~> 3.0.3) - PINRemoteImage/Core - - SwiftFormat/CLI (0.48.17) - - SwiftLint (0.45.0) + - SwiftFormat/CLI (0.48.18) + - SwiftLint (0.45.1) - SwiftyRSA (1.7.0): - SwiftyRSA/ObjC (= 1.7.0) - SwiftyRSA/ObjC (1.7.0) @@ -64,8 +64,8 @@ SPEC CHECKSUMS: PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086 PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20 PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01 - SwiftFormat: 0a9044eb365d74d4a0a2cefa5fe44a4cbef382a7 - SwiftLint: e5c7f1fba68eccfc51509d5b2ce1699f5502e0c7 + SwiftFormat: 7dd2b33a0a3d61095b61c911b6d89ff962ae695c + SwiftLint: 06ac37e4d38c7068e0935bb30cda95f093bec761 SwiftyRSA: 8c6dd1ea7db1b8dc4fb517a202f88bb1354bc2c6 Texture: 2e8ab2519452515f7f5a520f5a8f7e0a413abfa3