diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 8f86959dc..f4b9a64e1 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -38,7 +38,6 @@ 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */; }; 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */; }; 2C2D0B95275FDF6B0052771D /* Version6SchemaMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2D0B94275FDF6B0052771D /* Version6SchemaMigration.swift */; }; - 2C339B07275CB136005DEA79 /* FatalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C339B06275CB136005DEA79 /* FatalErrorViewController.swift */; }; 2C4E60F72757D91A00DE5770 /* EncryptedStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4E60F62757D91A00DE5770 /* EncryptedStorageMock.swift */; }; 2C60AB0C272564D40040D7F2 /* InvalidStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */; }; 2CAF25322756C37E005C7C7C /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF25312756C37E005C7C7C /* AppContext.swift */; }; @@ -62,10 +61,15 @@ 32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32DCA9701B2D5052225A0414 /* SignInViewController.swift */; }; 50531BE42629B9A80039BAE9 /* AttachmentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */; }; 5109A77C272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */; }; + 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */; }; + 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */; }; 512C1414271077F8002DE13F /* GoogleAPIClientForREST_PeopleService in Frameworks */ = {isa = PBXBuildFile; productRef = 512C1413271077F8002DE13F /* GoogleAPIClientForREST_PeopleService */; }; 5133B6702716320F00C95463 /* ContactKeyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */; }; 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */; }; 5133B6742716E5EA00C95463 /* LabelCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133B6732716E5EA00C95463 /* LabelCellNode.swift */; }; + 514C34DB276CE19C00FCAB79 /* ComposeMessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */; }; + 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; }; + 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; }; 5168FB0B274F94D300131072 /* MessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5168FB0A274F94D300131072 /* MessageAttachment.swift */; }; 51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; }; 51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C38270C7D2400D7C944 /* StorageMethod.swift */; }; @@ -111,8 +115,8 @@ 5ADEDCBC23A4329000EC495E /* PublicKeyDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADEDCBB23A4329000EC495E /* PublicKeyDetailViewController.swift */; }; 5ADEDCBE23A4363700EC495E /* KeyDetailInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADEDCBD23A4363700EC495E /* KeyDetailInfoViewController.swift */; }; 5ADEDCC023A43B0800EC495E /* KeyDetailInfoViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADEDCBF23A43B0800EC495E /* KeyDetailInfoViewDecorator.swift */; }; - 606FE33A2745AA2E009DA039 /* AttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606FE3392745AA2E009DA039 /* AttachmentViewController.swift */; }; 601EEE31272B19D200FE445B /* CheckMailAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601EEE30272B19D200FE445B /* CheckMailAuthViewController.swift */; }; + 606FE33A2745AA2E009DA039 /* AttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 606FE3392745AA2E009DA039 /* AttachmentViewController.swift */; }; 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 */; }; @@ -454,7 +458,6 @@ 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceTests.swift; sourceTree = ""; }; 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseServiceMock.swift; sourceTree = ""; }; 2C2D0B94275FDF6B0052771D /* Version6SchemaMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version6SchemaMigration.swift; sourceTree = ""; }; - 2C339B06275CB136005DEA79 /* FatalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorViewController.swift; sourceTree = ""; }; 2C4E60F62757D91A00DE5770 /* EncryptedStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorageMock.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 = ""; }; @@ -489,9 +492,14 @@ 4F928D493732294B4E521900 /* Pods-FlowCryptUIApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.release.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.release.xcconfig"; sourceTree = ""; }; 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentNode.swift; sourceTree = ""; }; 5109A77B272153B400D2CEB9 /* LeftAlignedCollectionViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftAlignedCollectionViewFlowLayout.swift; sourceTree = ""; }; + 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePasswordCellNode.swift; sourceTree = ""; }; + 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonWithPaddingNode.swift; sourceTree = ""; }; 5133B66F2716320F00C95463 /* ContactKeyDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailViewController.swift; sourceTree = ""; }; 5133B6712716321F00C95463 /* ContactKeyDetailDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyDetailDecorator.swift; sourceTree = ""; }; 5133B6732716E5EA00C95463 /* LabelCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCellNode.swift; sourceTree = ""; }; + 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageContext.swift; sourceTree = ""; }; + 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = ""; }; + 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = ""; }; 5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = ""; }; 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = ""; }; 51775C38270C7D2400D7C944 /* StorageMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMethod.swift; sourceTree = ""; }; @@ -532,8 +540,8 @@ 5ADEDCBD23A4363700EC495E /* KeyDetailInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailInfoViewController.swift; sourceTree = ""; }; 5ADEDCBF23A43B0800EC495E /* KeyDetailInfoViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyDetailInfoViewDecorator.swift; sourceTree = ""; }; 5ADEDCC123A43C6800EC495E /* KeyTextCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyTextCellNode.swift; sourceTree = ""; }; - 606FE3392745AA2E009DA039 /* AttachmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentViewController.swift; sourceTree = ""; }; 601EEE30272B19D200FE445B /* CheckMailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckMailAuthViewController.swift; sourceTree = ""; }; + 606FE3392745AA2E009DA039 /* AttachmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentViewController.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 /* SessionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionService.swift; sourceTree = ""; }; @@ -1371,7 +1379,10 @@ isa = PBXGroup; children = ( 9F6F3BEC26ADF5DE005BD9C6 /* ComposeMessageService.swift */, + 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */, + 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */, 9F6F3BED26ADF5DE005BD9C6 /* ComposeMessageError.swift */, + 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */, ); path = "Compose Message Service"; sourceTree = ""; @@ -2004,6 +2015,7 @@ 5A39F433239EC61C001F4607 /* TitleCellNode.swift */, 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, + 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */, 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, @@ -2031,6 +2043,7 @@ children = ( 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */, 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */, + 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */, 9FA1988F253C841F008C9CF2 /* TableViewController.swift */, 9F696292236091DD003712E1 /* SignInImageNode.swift */, 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */, @@ -2568,6 +2581,7 @@ C132B9CB1EC2DE6400763715 /* GeneralConstants.swift in Sources */, 5ADEDCBE23A4363700EC495E /* KeyDetailInfoViewController.swift in Sources */, D20D3C752520AB9A00D4AA9A /* BackupService.swift in Sources */, + 514C34DB276CE19C00FCAB79 /* ComposeMessageContext.swift in Sources */, C192421F1EC48B6900C3D251 /* SetupBackupsViewController.swift in Sources */, 9F0C3C2623194E0A00299985 /* FolderViewModel.swift in Sources */, F8678DCC2722143300BB1710 /* GmailService+draft.swift in Sources */, @@ -2589,6 +2603,7 @@ 9F31AB932329950800CF87EA /* Imap+Backup.swift in Sources */, 9F589F15238C8249007FD759 /* KeyChainService.swift in Sources */, 2CC50FB12744167A0051629A /* Folder.swift in Sources */, + 514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */, D2F41373243CC7990066AFB5 /* UserRealmObject.swift in Sources */, F80E95362720B6640093F243 /* DraftsListProvider.swift in Sources */, 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */, @@ -2751,6 +2766,7 @@ 5ADEDCC023A43B0800EC495E /* KeyDetailInfoViewDecorator.swift in Sources */, D227C0E6250538780070F805 /* RemoteFoldersProvider.swift in Sources */, 2CAF25322756C37E005C7C7C /* AppContext.swift in Sources */, + 514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */, 2CAF25342756C3A6005C7C7C /* ImapSessionProvider.swift in Sources */, 9F2AC5B1267BDED100F6149B /* GmailSearchExpressionGenerator.swift in Sources */, 9F953E09238310D500AEB98B /* KeyMethods.swift in Sources */, @@ -2764,6 +2780,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */, 9F7ECCA7272C3FB4008A1770 /* TextImageNode.swift in Sources */, D27177452424D44200BDA9A9 /* ComposeButtonNode.swift in Sources */, D24FAFAB2520BFAE00BF46C5 /* CheckBoxNode.swift in Sources */, @@ -2786,6 +2803,7 @@ D24FAFA42520BF9100BF46C5 /* CheckBoxCircleView.swift in Sources */, D2CDC3D72404704D002B045F /* RecipientEmailsCellNode.swift in Sources */, D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */, + 511D07E12769FBBA0050417B /* MessagePasswordCellNode.swift in Sources */, D211CE7B23FC59ED00D1CE38 /* InfoCellNode.swift in Sources */, D2E26F7224F26FFF00612AF1 /* ContactUserCellNode.swift in Sources */, D211CE6F23FC358000D1CE38 /* ButtonNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 73b65e1bd..e6aa0ba6b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -31,12 +31,16 @@ final class ComposeViewController: TableNodeViewController { case main, searchEmails([String]) } - private enum RecipientParts: Int, CaseIterable { - case recipient, recipientsInput, recipientDivider + private enum Section: Int, CaseIterable { + case recipient, password, compose, attachments } - private enum ComposeParts: Int, CaseIterable { - case subject, subjectDivider, text + private enum RecipientPart: Int, CaseIterable { + case list, input + } + + private enum ComposePart: Int, CaseIterable { + case topDivider, subject, subjectDivider, text } private let appContext: AppContext @@ -52,10 +56,14 @@ final class ComposeViewController: TableNodeViewController { private let router: GlobalRouterType private let clientConfiguration: ClientConfiguration - private let search = PassthroughSubject() - private let email: String + private var isMessagePasswordSupported: Bool { + guard let domain = email.emailParts?.domain else { return false } + let senderDomainsWithMessagePasswordSupport = ["flowcrypt.com"] + return senderDomainsWithMessagePasswordSupport.contains(domain) + } + private let search = PassthroughSubject() private var cancellable = Set() private var input: ComposeMessageInput @@ -67,6 +75,7 @@ final class ComposeViewController: TableNodeViewController { private weak var saveDraftTimer: Timer? private var composedLatestDraft: ComposedDraft? + private var messagePasswordAlertController: UIAlertController? private var didLayoutSubviews = false private var topContentInset: CGFloat { navigationController?.navigationBar.frame.maxY ?? 0 @@ -323,16 +332,16 @@ extension ComposeViewController { extension ComposeViewController { private func setupSearch() { search - .debounce(for: .milliseconds(400), scheduler: RunLoop.main) + .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() - .compactMap { [weak self] in - guard $0.isNotEmpty else { - self?.updateState(with: .main) - return nil - } - return $0 + .map { [weak self] query -> String in + if query.isEmpty { self?.updateState(with: .main) } + return query } - .sink { [weak self] in self?.searchEmail(with: $0) } + .sink(receiveValue: { [weak self] in + guard $0.isNotEmpty else { return } + self?.searchEmail(with: $0) + }) .store(in: &cancellable) } } @@ -400,6 +409,10 @@ extension ComposeViewController { private func handleSendTap() { Task { do { + guard contextToSend.hasMessagePasswordIfNeeded else { + throw MessageValidationError.noPubRecipients + } + let key = try await prepareSigningKey() try await sendMessage(key) } catch { @@ -505,7 +518,13 @@ extension ComposeViewController { let hideSpinnerAnimationDuration: TimeInterval = 1 DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in - self?.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + guard let self = self else { return } + + if case MessageValidationError.noPubRecipients = error, self.isMessagePasswordSupported { + self.setMessagePassword() + } else { + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } } } @@ -522,21 +541,21 @@ extension ComposeViewController { extension ComposeViewController: ASTableDelegate, ASTableDataSource { func numberOfSections(in _: ASTableNode) -> Int { - 3 + Section.allCases.count } func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { switch (state, section) { - case (.main, 0): - return RecipientParts.allCases.count - case (.main, 1): - return ComposeParts.allCases.count - case (.main, 2): + case (_, Section.recipient.rawValue): + return RecipientPart.allCases.count + case (.main, Section.password.rawValue): + return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 + case (.main, Section.compose.rawValue): + return ComposePart.allCases.count + case (.main, Section.attachments.rawValue): return contextToSend.attachments.count - case (.searchEmails, 0): - return RecipientParts.allCases.count case let (.searchEmails(emails), 1): - return emails.isNotEmpty ? emails.count : 1 + return emails.isNotEmpty ? emails.count + 1 : 2 case (.searchEmails, 2): return cloudContactProvider.isContactsScopeEnabled ? 0 : 2 default: @@ -550,28 +569,30 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { guard let self = self else { return ASCellNode() } switch (self.state, indexPath.section) { - case (_, 0): - guard let part = RecipientParts(rawValue: indexPath.row) else { return ASCellNode() } + case (_, Section.recipient.rawValue): + guard let part = RecipientPart(rawValue: indexPath.row) else { return ASCellNode() } switch part { - case .recipientDivider: return DividerCellNode() - case .recipientsInput: return self.recipientInput() - case .recipient: return self.recipientsNode() + case .input: return self.recipientInput() + case .list: return self.recipientsNode() } - case (.main, 1): - guard let composePart = ComposeParts(rawValue: indexPath.row) else { return ASCellNode() } - switch composePart { + case (.main, Section.password.rawValue): + return self.messagePasswordNode() + case (.main, Section.compose.rawValue): + guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } + switch part { case .subject: return self.subjectNode() case .text: return self.textNode() - case .subjectDivider: return DividerCellNode() + case .topDivider, .subjectDivider: return DividerCellNode() } - case (.main, 2): + case (.main, Section.attachments.rawValue): guard !self.contextToSend.attachments.isEmpty else { return ASCellNode() } return self.attachmentNode(for: indexPath.row) case let (.searchEmails(emails), 1): + guard indexPath.row > 0 else { return DividerCellNode() } guard emails.isNotEmpty else { return self.noSearchResultsNode() } - return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row])) + return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row-1])) case (.searchEmails, 2): return indexPath.row == 0 ? DividerCellNode() : self.enableGoogleContactsNode() default: @@ -584,23 +605,21 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { if case let .searchEmails(emails) = state { switch indexPath.section { case 1: - let selectedEmail = emails[safe: indexPath.row] + let selectedEmail = emails[safe: indexPath.row-1] handleEndEditingAction(with: selectedEmail) case 2: askForContactsPermission() default: break } - } else { - if tableNode.nodeForRow(at: indexPath) is AttachmentNode { - let controller = AttachmentViewController( - file: contextToSend.attachments[indexPath.row], - shouldShowDownloadButton: false - ) - navigationController?.pushViewController(controller, animated: true ) - } + } else if tableNode.nodeForRow(at: indexPath) is AttachmentNode { + let controller = AttachmentViewController( + file: contextToSend.attachments[indexPath.row], + shouldShowDownloadButton: false + ) + navigationController?.pushViewController(controller, animated: true ) } - } + } } // MARK: - Nodes @@ -634,6 +653,17 @@ extension ComposeViewController { } } + private func messagePasswordNode() -> ASCellNode { + let input = contextToSend.hasMessagePassword + ? decorator.styledFilledMessagePasswordInput() + : decorator.styledEmptyMessagePasswordInput() + + return MessagePasswordCellNode( + input: input, + setMessagePassword: { [weak self] in self?.setMessagePassword() } + ) + } + private func textNode() -> ASCellNode { let styledQuote = decorator.styledQuote(with: input) let height = max(decorator.frame(for: styledQuote).height, 40) @@ -723,7 +753,7 @@ extension ComposeViewController { ), onDeleteTap: { [weak self] in self?.contextToSend.attachments.safeRemove(at: index) - self?.node.reloadSections(IndexSet(integer: 2), with: .automatic) + self?.node.reloadSections([Section.attachments.rawValue], with: .automatic) } ) } @@ -750,11 +780,15 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { private var textField: TextFieldNode? { - (node.nodeForRow(at: IndexPath(row: RecipientParts.recipientsInput.rawValue, section: 0)) as? TextFieldCellNode)?.textField + let indexPath = IndexPath( + row: RecipientPart.input.rawValue, + section: Section.recipient.rawValue + ) + return (node.nodeForRow(at: indexPath) as? TextFieldCellNode)?.textField } private var recipientsIndexPath: IndexPath { - IndexPath(row: RecipientParts.recipient.rawValue, section: 0) + IndexPath(row: RecipientPart.list.rawValue, section: Section.recipient.rawValue) } private var recipients: [ComposeMessageRecipient] { @@ -763,7 +797,7 @@ extension ComposeViewController { private func shouldChange(with textField: UITextField, and character: String) -> Bool { func nextResponder() { - guard let node = node.visibleNodes[safe: ComposeParts.subject.rawValue] as? TextFieldCellNode else { return } + guard let node = node.visibleNodes[safe: ComposePart.subject.rawValue] as? TextFieldCellNode else { return } node.becomeFirstResponder() } @@ -839,6 +873,7 @@ extension ComposeViewController { // reset textfield textField?.reset() node.view.keyboardDismissMode = .interactive + search.send("") updateState(with: .main) } @@ -852,7 +887,8 @@ extension ComposeViewController { guard selectedRecipients.isEmpty else { // remove selected recipients contextToSend.recipients = recipients.filter { !$0.state.isSelected } - node.reloadRows(at: [recipientsIndexPath], with: .fade) + node.reloadSections([Section.recipient.rawValue, Section.password.rawValue], + with: .automatic) return } @@ -862,6 +898,7 @@ extension ComposeViewController { last.state = self.decorator.recipientSelectedState contextToSend.recipients.append(last) node.reloadRows(at: [recipientsIndexPath], with: .fade) + node.reloadSections([Section.password.rawValue], with: .automatic) } else { // dismiss keyboard if no recipients left textField.resignFirstResponder() @@ -869,12 +906,7 @@ extension ComposeViewController { } private func handleEditingChanged(with text: String?) { - guard let text = text, text.isNotEmpty else { - search.send("") - return - } - - search.send(text) + search.send(text ?? "") } private func handleDidBeginEditing() { @@ -895,7 +927,7 @@ extension ComposeViewController { private func evaluate(recipient: ComposeMessageRecipient) { guard recipient.email.isValidEmail else { - handleEvaluation(for: recipient, with: self.decorator.recipientInvalidEmailState) + handleEvaluation(for: recipient, with: self.decorator.recipientInvalidEmailState, keyState: nil) return } @@ -903,7 +935,7 @@ extension ComposeViewController { do { let contact = try await service.searchContact(with: recipient.email) let state = getRecipientState(from: contact) - handleEvaluation(for: recipient, with: state) + handleEvaluation(for: recipient, with: state, keyState: contact.keyState) } catch { handleEvaluation(error: error, with: recipient) } @@ -923,9 +955,12 @@ extension ComposeViewController { } } - private func handleEvaluation(for recipient: ComposeMessageRecipient, with state: RecipientState) { + private func handleEvaluation(for recipient: ComposeMessageRecipient, + with state: RecipientState, + keyState: PubKeyState?) { updateRecipientWithNew( state: state, + keyState: keyState, for: .left(recipient) ) } @@ -942,11 +977,14 @@ extension ComposeViewController { updateRecipientWithNew( state: recipientState, + keyState: nil, for: .left(recipient) ) } - private func updateRecipientWithNew(state: RecipientState, for context: Either) { + private func updateRecipientWithNew(state: RecipientState, + keyState: PubKeyState?, + for context: Either) { let index: Int? = { switch context { case let .left(recipient): @@ -958,7 +996,10 @@ extension ComposeViewController { guard let recipientIndex = index else { return } contextToSend.recipients[recipientIndex].state = state - node.reloadRows(at: [recipientsIndexPath], with: .fade) + contextToSend.recipients[recipientIndex].keyState = keyState + + node.reloadSections([Section.password.rawValue], with: .automatic) + node.reloadRows(at: [recipientsIndexPath], with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath) { @@ -972,7 +1013,8 @@ extension ComposeViewController { contextToSend.recipients[indexPath.row].state = decorator.recipientSelectedState } - node.reloadRows(at: [recipientsIndexPath], with: .fade) + node.reloadRows(at: [recipientsIndexPath], with: .automatic) + if !(textField?.isFirstResponder() ?? true) { textField?.becomeFirstResponder() } @@ -988,7 +1030,9 @@ extension ComposeViewController { break case let .error(_, isRetryError): if isRetryError { - updateRecipientWithNew(state: decorator.recipientIdleState, for: .right(indexPath)) + updateRecipientWithNew(state: decorator.recipientIdleState, + keyState: nil, + for: .right(indexPath)) evaluate(recipient: recipient) } else { contextToSend.recipients.remove(at: indexPath.row) @@ -996,6 +1040,53 @@ extension ComposeViewController { } } } + + private func setMessagePassword() { + Task { + contextToSend.messagePassword = await enterMessagePassword() + node.reloadSections([Section.password.rawValue], with: .automatic) + } + } + + private func enterMessagePassword() async -> String? { + return await withCheckedContinuation { (continuation: CheckedContinuation) in + self.messagePasswordAlertController = createMessagePasswordAlert(continuation: continuation) + self.present(self.messagePasswordAlertController!, animated: true, completion: nil) + } + } + + private func createMessagePasswordAlert(continuation: CheckedContinuation) -> UIAlertController { + let alert = UIAlertController( + title: "compose_password_modal_title".localized, + message: "compose_password_modal_message".localized, + preferredStyle: .alert + ) + + alert.addTextField { [weak self] in + guard let self = self else { return } + $0.isSecureTextEntry = true + $0.text = self.contextToSend.messagePassword + $0.accessibilityLabel = "aid-message-password-textfield" + $0.addTarget(self, action: #selector(self.messagePasswordTextFieldDidChange), for: .editingChanged) + } + + let cancelAction = UIAlertAction(title: "cancel".localized, style: .cancel) { _ in + return continuation.resume(returning: self.contextToSend.messagePassword) + } + alert.addAction(cancelAction) + + let setAction = UIAlertAction(title: "set".localized, style: .default) { _ in + return continuation.resume(returning: alert.textFields?[0].text) + } + setAction.isEnabled = contextToSend.hasMessagePassword + alert.addAction(setAction) + + return alert + } + + @objc private func messagePasswordTextFieldDidChange(_ sender: UITextField) { + messagePasswordAlertController?.actions[1].isEnabled = (sender.text ?? "").isNotEmpty + } } // MARK: - State Handling @@ -1007,7 +1098,8 @@ extension ComposeViewController { case .main: node.reloadData() case .searchEmails: - node.reloadSections([1, 2], with: .automatic) + let sections: [Section] = [.password, .compose, .attachments] + node.reloadSections(IndexSet(sections.map(\.rawValue)), with: .automatic) } } } @@ -1022,7 +1114,7 @@ extension ComposeViewController: UIDocumentPickerDelegate { return } appendAttachmentIfAllowed(attachment) - node.reloadSections(IndexSet(integer: 2), with: .automatic) + node.reloadSections([Section.attachments.rawValue], with: .automatic) } } @@ -1079,7 +1171,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } appendAttachmentIfAllowed(composeMessageAttachment) - node.reloadSections(IndexSet(integer: 2), with: .automatic) + node.reloadSections([Section.attachments.rawValue], with: .automatic) } } @@ -1102,7 +1194,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo return } appendAttachmentIfAllowed(attachment) - node.reloadSections(IndexSet(integer: 2), with: .automatic) + node.reloadSections([Section.attachments.rawValue], with: .automatic) } private func appendAttachmentIfAllowed(_ attachment: MessageAttachment) { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index a5adcab72..3bacc9c54 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -62,7 +62,7 @@ struct ComposeViewDecorator { InfoCellNode.Input( attributedText: email.attributed( .medium(17), - color: UIColor.mainTextColor.withAlphaComponent(0.8), + color: .mainTextColor.withAlphaComponent(0.8), alignment: .left ), image: nil, @@ -98,10 +98,36 @@ struct ComposeViewDecorator { return (text + message).attributed(.regular(17)) } + func styledEmptyMessagePasswordInput() -> MessagePasswordCellNode.Input { + messagePasswordInput( + text: "compose_password_placeholder".localized, + color: .warningColor, + imageName: "lock" + ) + } + + func styledFilledMessagePasswordInput() -> MessagePasswordCellNode.Input { + messagePasswordInput( + text: "compose_password_set_message".localized, + color: .main, + imageName: "checkmark.circle" + ) + } + + private func messagePasswordInput(text: String, + color: UIColor, + imageName: String) -> MessagePasswordCellNode.Input { + .init( + text: text.attributed(.regular(14), color: color), + color: color, + image: UIImage(systemName: imageName)?.tinted(color) + ) + } + func frame(for string: NSAttributedString, insets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 0, right: 8)) -> CGRect { let width = UIScreen.main.bounds.width - insets.left - insets.right - let maxSize = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) + let maxSize = CGSize(width: width, height: .greatestFiniteMagnitude) return string.boundingRect(with: maxSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil) @@ -111,30 +137,30 @@ struct ComposeViewDecorator { // MARK: - Color extension UIColor { static var titleNodeBackgroundColorSelected: UIColor { - UIColor.colorFor( - darkStyle: UIColor.lightGray, - lightStyle: UIColor.black.withAlphaComponent(0.1) + colorFor( + darkStyle: lightGray, + lightStyle: black.withAlphaComponent(0.1) ) } static var titleNodeBackgroundColor: UIColor { - UIColor.colorFor( - darkStyle: UIColor.darkGray.withAlphaComponent(0.5), - lightStyle: UIColor.white.withAlphaComponent(0.9) + colorFor( + darkStyle: darkGray.withAlphaComponent(0.5), + lightStyle: white.withAlphaComponent(0.9) ) } static var borderColorSelected: UIColor { - UIColor.colorFor( - darkStyle: UIColor.white.withAlphaComponent(0.5), + colorFor( + darkStyle: white.withAlphaComponent(0.5), lightStyle: black.withAlphaComponent(0.4) ) } static var borderColor: UIColor { - UIColor.colorFor( - darkStyle: UIColor.white.withAlphaComponent(0.5), - lightStyle: UIColor.black.withAlphaComponent(0.3) + colorFor( + darkStyle: white.withAlphaComponent(0.5), + lightStyle: black.withAlphaComponent(0.3) ) } } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index d59d38328..14d91ce96 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -183,7 +183,8 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { "atts": msg.atts.map { att in ["name": att.name, "type": att.type, "base64": att.base64] }, "format": fmt.rawValue, "pubKeys": msg.pubKeys, - "signingPrv": signingPrv + "signingPrv": signingPrv, + "pwd": msg.password ], data: nil) return CoreRes.ComposeEmail(mimeEncoded: r.data) } @@ -246,7 +247,7 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { ]) while callbackResults[callbackId] == nil { - await Task.sleep(1_000_000) // 1ms + try await Task.sleep(nanoseconds: 1_000_000) // 1ms } guard diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index cdc11c9d4..ee61b0bfa 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -135,6 +135,7 @@ struct SendableMsg: Equatable { let atts: [Attachment] let pubKeys: [String]? let signingPrv: PrvKeyInfo? + let password: String? } struct DecryptErr: Decodable { diff --git a/FlowCrypt/Extensions/Error+Extension.swift b/FlowCrypt/Extensions/Error+Extension.swift index cc8c7f10b..08082fbe0 100644 --- a/FlowCrypt/Extensions/Error+Extension.swift +++ b/FlowCrypt/Extensions/Error+Extension.swift @@ -12,7 +12,7 @@ extension Error { var errorMessage: String { switch self { case let self as CustomStringConvertible: - return self.description + return String(describing: self) default: return localizedDescription } diff --git a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift index b5b90d7b4..a119ef287 100644 --- a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift +++ b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift @@ -61,7 +61,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { func getActiveFesUrl(for email: String) async throws -> String? { do { - guard let userDomain = email.recipientDomain, + guard let userDomain = email.emailParts?.domain, !EnterpriseServerApi.publicEmailProviderDomains.contains(userDomain) else { return nil } @@ -94,7 +94,7 @@ class EnterpriseServerApi: EnterpriseServerApiType { } func getClientConfiguration(for email: String) async throws -> RawClientConfiguration { - guard let userDomain = email.recipientDomain else { + guard let userDomain = email.emailParts?.domain else { throw EnterpriseServerApiError.emailFormat } diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 3d0500523..e004e38fc 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -62,7 +62,8 @@ extension BackupService: BackupServiceType { replyToMimeMsg: nil, atts: attachments, pubKeys: nil, - signingPrv: nil) + signingPrv: nil, + password: nil) let t = try await core.composeEmail(msg: message, fmt: .plain) try await messageSender.sendMail(input: MessageGatewayInput(mime: t.mimeEncoded, threadId: nil), diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift index fcd5ef061..250ccabd7 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift @@ -121,15 +121,15 @@ class ClientConfiguration { /// Some orgs have a list of email domains where they do NOT want such emails to be looked up on public sources (such as Attester) /// This is because they already have other means to obtain public keys for these domains, such as from their own internal keyserver - func canLookupThisRecipientOnAttester(recipient email: String) throws -> Bool { + func canLookupThisRecipientOnAttester(recipient: String) throws -> Bool { let disallowedDomains = raw.disallowAttesterSearchForDomains ?? [] if disallowedDomains.contains("*") { return false } - guard let recipientDomain = email.recipientDomain else { - throw AppErr.general("organisational_wrong_email_error".localizeWithArguments(email)) + guard let recipientDomain = recipient.emailParts?.domain else { + throw AppErr.general("organisational_wrong_email_error".localizeWithArguments(recipient)) } return !disallowedDomains.contains(recipientDomain) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift new file mode 100644 index 000000000..ca8ce8d97 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -0,0 +1,54 @@ +// +// ComposeMessageContext.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 17/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct ComposeMessageContext: Equatable { + var message: String? + var recipients: [ComposeMessageRecipient] = [] + var subject: String? + var attachments: [MessageAttachment] = [] + var messagePassword: String? { + get { + (_messagePassword ?? "").isNotEmpty ? _messagePassword : nil + } + set { _messagePassword = newValue } + } + + private var _messagePassword: String? +} + +extension ComposeMessageContext { + init(message: String? = nil, + recipients: [ComposeMessageRecipient] = [], + subject: String? = nil, + attachments: [MessageAttachment] = [], + messagePassword: String? = nil + ) { + self.message = message + self.recipients = recipients + self.subject = subject + self.attachments = attachments + self.messagePassword = messagePassword + } +} + +extension ComposeMessageContext { + var hasMessagePassword: Bool { + messagePassword != nil + } + + var hasRecipientsWithoutPubKey: Bool { + recipients.first { $0.keyState == .empty } != nil + } + + var hasMessagePasswordIfNeeded: Bool { + guard hasRecipientsWithoutPubKey else { return true } + return hasMessagePassword + } +} diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift new file mode 100644 index 000000000..18094f5f9 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift @@ -0,0 +1,21 @@ +// +// ComposeMessageRecipient.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 17/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +struct ComposeMessageRecipient { + let email: String + var state: RecipientState + var keyState: PubKeyState? +} + +extension ComposeMessageRecipient: Equatable { + static func == (lhs: ComposeMessageRecipient, rhs: ComposeMessageRecipient) -> Bool { + return lhs.email == rhs.email + } +} diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService+State.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService+State.swift new file mode 100644 index 000000000..ee1c0f4da --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService+State.swift @@ -0,0 +1,41 @@ +// +// ComposeMessageService+State.swift +// FlowCrypt +// +// Created by Roma Sosnovsky on 17/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import Foundation + +extension ComposeMessageService { + enum State { + case idle + case validatingMessage + case startComposing + case progressChanged(Float) + case messageSent + + var message: String? { + switch self { + case .idle: + return nil + case .validatingMessage: + return "validating_title".localized + case .startComposing: + return "encrypting_title".localized + case .progressChanged: + return "compose_uploading".localized + case .messageSent: + return "compose_message_sent".localized + } + } + + var progress: Float? { + guard case .progressChanged(let progress) = self else { + return nil + } + return progress + } + } +} diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index f7e048849..b324aaff1 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -13,22 +13,6 @@ import FlowCryptCommon typealias RecipientState = RecipientEmailsCellNode.Input.State -struct ComposeMessageContext: Equatable { - var message: String? - var recipients: [ComposeMessageRecipient] = [] - var subject: String? - var attachments: [MessageAttachment] = [] -} - -struct ComposeMessageRecipient: Equatable { - let email: String - var state: RecipientState - - static func == (lhs: ComposeMessageRecipient, rhs: ComposeMessageRecipient) -> Bool { - return lhs.email == rhs.email - } -} - protocol CoreComposeMessageType { func composeEmail(msg: SendableMsg, fmt: MsgFmt) async throws -> CoreRes.ComposeEmail } @@ -109,7 +93,11 @@ final class ComposeMessageService { ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } : [] - let allRecipientPubs = try await getPubKeys(for: recipients) + let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) + let validPubKeys = try validate( + recipients: recipientsWithPubKeys, + hasMessagePassword: contextToSend.hasMessagePassword + ) let replyToMimeMsg = input.replyToMime .flatMap { String(data: $0, encoding: .utf8) } @@ -122,36 +110,48 @@ final class ComposeMessageService { subject: subject, replyToMimeMsg: replyToMimeMsg, atts: sendableAttachments, - pubKeys: [myPubKey] + allRecipientPubs, - signingPrv: signingPrv + pubKeys: [myPubKey] + validPubKeys, + signingPrv: signingPrv, + password: contextToSend.messagePassword ) } - private func getPubKeys(for recipients: [ComposeMessageRecipient]) async throws -> [String] { + private func isMessagePasswordSupported(for email: String) -> Bool { + guard let senderDomain = email.emailParts?.domain else { return false } + let senderDomainsWithMessagePasswordSupport = ["flowcrypt.com"] + return senderDomainsWithMessagePasswordSupport.contains(senderDomain) + } + + private func getRecipientKeys(for recipients: [ComposeMessageRecipient]) async throws -> [RecipientWithSortedPubKeys] { var recipientsWithKeys: [RecipientWithSortedPubKeys] = [] for recipient in recipients { let armoredPubkeys = contactsService.retrievePubKeys(for: recipient.email).joined(separator: "\n") let parsed = try await self.core.parseKeys(armoredOrBinary: armoredPubkeys.data()) recipientsWithKeys.append(RecipientWithSortedPubKeys(email: recipient.email, keyDetails: parsed.keyDetails)) } - return try validate(recipients: recipientsWithKeys) + return recipientsWithKeys } - private func validate(recipients: [RecipientWithSortedPubKeys]) throws -> [String] { + private func validate(recipients: [RecipientWithSortedPubKeys], + hasMessagePassword: Bool) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.first(where: { $0.keyState == keyState }) != nil } + logger.logDebug("validate recipients: \(recipients)") - logger.logDebug("validate recipient keyStates: \(recipients.map { $0.keyState })") - guard !contains(keyState: .empty) else { + logger.logDebug("validate recipient keyStates: \(recipients.map(\.keyState))") + + guard hasMessagePassword || !contains(keyState: .empty) else { throw MessageValidationError.noPubRecipients } + guard !contains(keyState: .expired) else { throw MessageValidationError.expiredKeyRecipients } guard !contains(keyState: .revoked) else { throw MessageValidationError.revokedKeyRecipients } + return recipients.flatMap(\.activePubKeys).map(\.armored) } @@ -194,35 +194,3 @@ final class ComposeMessageService { } } } - -extension ComposeMessageService { - enum State { - case idle - case validatingMessage - case startComposing - case progressChanged(Float) - case messageSent - - var message: String? { - switch self { - case .idle: - return nil - case .validatingMessage: - return "validating_title".localized - case .startComposing: - return "encrypting_title".localized - case .progressChanged: - return "compose_uploading".localized - case .messageSent: - return "compose_message_sent".localized - } - } - - var progress: Float? { - guard case .progressChanged(let progress) = self else { - return nil - } - return progress - } - } -} diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift index b228c22f7..ccd487743 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift @@ -42,7 +42,8 @@ class WkdApi: WkdApiType { func lookup(email: String) async throws -> [KeyDetails] { guard - !EnterpriseServerApi.publicEmailProviderDomains.contains(email.recipientDomain ?? ""), + let domain = email.emailParts?.domain, + !EnterpriseServerApi.publicEmailProviderDomains.contains(domain), 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 3949bcd71..5bfbaffb8 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -9,6 +9,7 @@ "retry_title" = "Retry"; "ok" = "Ok"; "cancel" = "Cancel"; +"set" = "Set"; "open" = "Open"; "settings" = "Settings"; "continue" = "Continue"; @@ -78,6 +79,10 @@ "compose_recipient_revoked" = "One or more of your recipients have revoked public keys (marked in red).\n\nPlease ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin."; "compose_recipient_expired" = "One or more of your recipients have expired public keys (marked in orange).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin."; "compose_recipient_invalid_email" = "One or more of your recipients have invalid email address (marked in red)"; +"compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up."; +"compose_password_set_message" = "Web portal password added"; +"compose_password_modal_title" = "Set web portal password"; +"compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)"; "compose_error" = "Could not compose message"; "compose_reply_successful" = "Reply successfully sent"; "compose_quote_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 0f3e1ad66..e5f54f610 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -114,7 +114,8 @@ final class FlowCryptCoreTests: XCTestCase { replyToMimeMsg: nil, atts: [], pubKeys: nil, - signingPrv: nil + signingPrv: nil, + password: nil ) let r = try await core.composeEmail(msg: msg, fmt: .plain) let mime = String(data: r.mimeEncoded, encoding: .utf8)! @@ -135,7 +136,8 @@ final class FlowCryptCoreTests: XCTestCase { replyToMimeMsg: nil, atts: [], pubKeys: [TestData.k0.pub, TestData.k1.pub], - signingPrv: nil + signingPrv: nil, + password: nil ) let r = try await self.core.composeEmail(msg: msg, fmt: .encryptInline) let mime = String(data: r.mimeEncoded, encoding: .utf8)! @@ -161,7 +163,8 @@ final class FlowCryptCoreTests: XCTestCase { subject: "subj", replyToMimeMsg: nil, atts: [attachment], pubKeys: [TestData.k0.pub, TestData.k1.pub], - signingPrv: nil + signingPrv: nil, + password: nil ) let r = try await core.composeEmail(msg: msg, fmt: .encryptInline) let mime = String(data: r.mimeEncoded, encoding: .utf8)! @@ -191,7 +194,8 @@ final class FlowCryptCoreTests: XCTestCase { replyToMimeMsg: nil, atts: [], pubKeys: [k.public], - signingPrv: nil + signingPrv: nil, + password: nil ) let mime = try await core.composeEmail(msg: msg, fmt: .encryptInline) let keys = [PrvKeyInfo(private: k.private!, longid: k.ids[0].longid, passphrase: passphrase, fingerprints: k.fingerprints)] diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index 36dc9468d..59d30cfb8 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -21,6 +21,15 @@ class ComposeMessageServiceTests: XCTestCase { ComposeMessageRecipient(email: "test3@gmail.com", state: recipientIdleState) ] let validKeyDetails = EncryptedStorageMock.createFakeKeyDetails(expiration: nil) + let keypair = Keypair( + primaryFingerprint: "", + private: "", + public: "public key", + passphrase: nil, + source: "", + allFingerprints: [], + allLongids: [] + ) var core = CoreComposeMessageMock() var encryptedStorage = EncryptedStorageMock() @@ -204,17 +213,7 @@ class ComposeMessageServiceTests: XCTestCase { } func testValidateMessageInputWithAllEmptyRecipientPubKeys() async { - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in [] @@ -242,17 +241,7 @@ class ComposeMessageServiceTests: XCTestCase { let keyDetails = EncryptedStorageMock.createFakeKeyDetails(expiration: Int(Date().timeIntervalSince1970 - 60)) return CoreRes.ParseKeys(format: .armored, keyDetails: [keyDetails]) } - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["pubKey"] @@ -280,17 +269,7 @@ class ComposeMessageServiceTests: XCTestCase { let keyDetails = EncryptedStorageMock.createFakeKeyDetails(expiration: nil, revoked: true) return CoreRes.ParseKeys(format: .armored, keyDetails: [keyDetails]) } - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["pubKey"] @@ -330,17 +309,7 @@ class ComposeMessageServiceTests: XCTestCase { } return CoreRes.ParseKeys(format: .armored, keyDetails: allKeyDetails) } - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["revoked", "expired", "valid"] @@ -377,24 +346,15 @@ class ComposeMessageServiceTests: XCTestCase { "valid", "valid" ], - signingPrv: nil) + signingPrv: nil, + password: nil) XCTAssertNotNil(result) XCTAssertEqual(result, expected) } func testValidateMessageInputWithoutOneRecipientPubKey() async throws { - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] let recWithoutPubKey = recipients[0].email recipients.forEach { _ in contactsService.retrievePubKeysResult = { recipient in @@ -423,17 +383,7 @@ class ComposeMessageServiceTests: XCTestCase { } func testSuccessfulMessageValidation() async throws { - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] - ) - ] + encryptedStorage.getKeypairsResult = [keypair] recipients.enumerated().forEach { element, index in contactsService.retrievePubKeysResult = { recipient in ["pubKey"] @@ -470,7 +420,8 @@ class ComposeMessageServiceTests: XCTestCase { "pubKey", "pubKey" ], - signingPrv: nil) + signingPrv: nil, + password: nil) XCTAssertNotNil(result) XCTAssertEqual(result, expected) diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index f1e887912..e8d6ebece 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -61,27 +61,9 @@ public extension NSAttributedString { // MARK: Email parsing public extension String { - var userAndRecipientDomain: (user: String, domain: String)? { + var emailParts: (username: String, domain: String)? { let parts = self.split(separator: "@") - if parts.count != 2 { - return nil - } + guard parts.count == 2 else { return nil } return (String(parts[0]), String(parts[1])) } - - var userEmail: String? { - let parts = self.split(separator: "@") - if parts.count != 2 { - return nil - } - return String(parts[0]) - } - - var recipientDomain: String? { - let parts = self.split(separator: "@") - if parts.count != 2 { - return nil - } - return String(parts[1]) - } } diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift new file mode 100644 index 000000000..7a3d32c08 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -0,0 +1,80 @@ +// +// MessagePasswordCellNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 15/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit +import UIKit + +public final class MessagePasswordCellNode: CellNode { + public struct Input { + let text: NSAttributedString? + let color: UIColor + let image: UIImage? + + public init(text: NSAttributedString?, + color: UIColor, + image: UIImage?) { + self.text = text + self.color = color + self.image = image + } + } + + private let input: Input + + private let buttonNode = ASButtonNode() + private let setMessagePassword: (() -> Void)? + + public init(input: Input, + setMessagePassword: (() -> Void)?) { + self.input = input + self.setMessagePassword = setMessagePassword + + super.init() + + automaticallyManagesSubnodes = true + + setupButtonNode() + } + + private func setupButtonNode() { + buttonNode.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) + buttonNode.borderColor = input.color.cgColor + buttonNode.borderWidth = 1 + buttonNode.cornerRadius = 6 + buttonNode.contentHorizontalAlignment = .left + buttonNode.accessibilityIdentifier = "aid-message-password-cell" + + buttonNode.setAttributedTitle(input.text, for: .normal) + buttonNode.setImage(input.image, for: .normal) + buttonNode.addTarget(self, action: #selector(onButtonTap), forControlEvents: .touchUpInside) + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + buttonNode.style.flexShrink = 1.0 + + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1.0 + + let spec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .start, + alignItems: .start, + children: [buttonNode, spacer] + ) + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), + child: spec + ) + } + + @objc private func onButtonTap() { + setMessagePassword?() + } +} diff --git a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift index 1ccad2831..d322cb871 100644 --- a/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift +++ b/FlowCryptUI/Cell Nodes/RecipientEmailNode.swift @@ -57,8 +57,8 @@ final class RecipientEmailNode: CellNode { break } } - imageNode.addTarget(self, action: #selector(handleTap(_:)), forControlEvents: .touchUpInside) - titleNode.addTarget(self, action: #selector(handleTap(_:)), forControlEvents: .touchUpInside) + imageNode.addTarget(self, action: #selector(handleTap), forControlEvents: .touchUpInside) + titleNode.addTarget(self, action: #selector(handleTap), forControlEvents: .touchUpInside) } @objc private func handleTap(_ sender: ASDisplayNode) { diff --git a/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift new file mode 100644 index 000000000..43e90966a --- /dev/null +++ b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift @@ -0,0 +1,39 @@ +// +// ButtonWithPaddingNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 15/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class ButtonWithPaddingNode: ASDisplayNode { + private let insets: UIEdgeInsets + private let buttonNode = ASTextNode2() + + public init( + text: NSAttributedString?, + insets: UIEdgeInsets, + backgroundColor: UIColor? = nil, + cornerRadius: CGFloat = 0, + action: (() -> Void)? + ) { + self.insets = insets + + super.init() + + automaticallyManagesSubnodes = true + buttonNode.attributedText = text + + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + ASInsetLayoutSpec( + insets: insets, + child: buttonNode + ) + } +} diff --git a/Gemfile.lock b/Gemfile.lock index 01f83a085..70f7ebe95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -17,17 +17,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.542.0) - aws-sdk-core (3.124.0) + aws-partitions (1.543.0) + aws-sdk-core (3.125.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.52.0) - aws-sdk-core (~> 3, >= 3.122.0) + aws-sdk-kms (1.53.0) + aws-sdk-core (~> 3, >= 3.125.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.109.0) - aws-sdk-core (~> 3, >= 3.122.0) + aws-sdk-s3 (1.110.0) + aws-sdk-core (~> 3, >= 3.125.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) diff --git a/appium/package.json b/appium/package.json index 646d86ab2..ee8dbe7fa 100644 --- a/appium/package.json +++ b/appium/package.json @@ -32,7 +32,7 @@ "@wdio/junit-reporter": "^7.16.12", "@wdio/local-runner": "^7.16.10", "@wdio/spec-reporter": "^7.16.9", - "appium": "1.22.1", + "appium": "1.22.2", "babel-eslint": "^10.1.0", "dotenv": "^10.0.0", "eslint": "^8.4.1", @@ -49,4 +49,4 @@ "wdio-video-reporter": "^3.1.3", "webdriverio": "^7.16.10" } -} +} \ No newline at end of file diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 7dbcfbd6f..6cd7329f6 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -81,7 +81,11 @@ export const CommonData = { firstAttachmentName: 'Screenshot_20180422_125217.png.asc' }, recipientWithoutPublicKey: { - email: 'no.publickey@flowcrypt.com' + email: 'no.publickey@flowcrypt.com', + password: '123456', + modalMessage: `Set web portal password\nThe recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)`, + emptyPasswordMessage: 'Tap to add password for recipients who don\'t have encryption set up.', + addedPasswordMessage: 'Web portal password added', }, recipientWithExpiredPublicKey: { email: 'expired@flowcrypt.com' diff --git a/appium/tests/screenobjects/mail-folder.screen.ts b/appium/tests/screenobjects/mail-folder.screen.ts index c839af3d5..ce7d20371 100644 --- a/appium/tests/screenobjects/mail-folder.screen.ts +++ b/appium/tests/screenobjects/mail-folder.screen.ts @@ -74,6 +74,7 @@ class MailFolderScreen extends BaseScreen { } clickCreateEmail = async () => { + await browser.pause(500); const elem = await this.createEmailButton; if ((await elem.isDisplayed()) !== true) { await TouchHelper.scrollDownToElement(elem); diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index f1046883a..64da7d7aa 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -10,12 +10,18 @@ const SELECTORS = { '/XCUIElementTypeOther/XCUIElementTypeOther[1]/XCUIElementTypeOther/XCUIElementTypeTable' + '/XCUIElementTypeCell[1]/XCUIElementTypeOther/XCUIElementTypeCollectionView/XCUIElementTypeCell' + '/XCUIElementTypeOther/XCUIElementTypeOther/XCUIElementTypeStaticText', //it works only with this selector + PASSWORD_CELL: '~aid-message-password-cell', ATTACHMENT_CELL: '~aid-attachment-cell-0', ATTACHMENT_NAME_LABEL: '~aid-attachment-title-label-0', DELETE_ATTACHMENT_BUTTON: '~aid-attachment-delete-button-0', RETURN_BUTTON: '~Return', + SET_PASSWORD_BUTTON: '~Set', + CANCEL_BUTTON: '~Cancel', BACK_BUTTON: '~aid-back-button', SEND_BUTTON: '~aid-compose-send', + MESSAGE_PASSWORD_MODAL: '~aid-message-password-modal', + MESSAGE_PASSWORD_TEXTFIELD: '~aid-message-password-textfield', + ALERT: "-ios predicate string:type == 'XCUIElementTypeAlert'" }; class NewMessageScreen extends BaseScreen { @@ -63,17 +69,43 @@ class NewMessageScreen extends BaseScreen { return $(SELECTORS.SEND_BUTTON); } + get passwordCell() { + return $(SELECTORS.PASSWORD_CELL); + } + + get passwordModal() { + return $(SELECTORS.MESSAGE_PASSWORD_MODAL); + } + + get currentModal() { + return $(SELECTORS.ALERT); + } + + get passwordTextField() { + return $(SELECTORS.MESSAGE_PASSWORD_TEXTFIELD); + } + + get setPasswordButton() { + return $(SELECTORS.SET_PASSWORD_BUTTON); + } + + get cancelButton() { + return $(SELECTORS.CANCEL_BUTTON); + } + setAddRecipient = async (recipient: string) => { await (await this.addRecipientField).setValue(recipient); - await browser.pause(2000); + await browser.pause(500); await (await $(SELECTORS.RETURN_BUTTON)).click() }; setSubject = async (subject: string) => { + await browser.pause(500); await ElementHelper.waitClickAndType(await this.subjectField, subject); }; setComposeSecurityMessage = async (message: string) => { + await browser.pause(500); await (await this.composeSecurityMessage).setValue(message); }; @@ -89,6 +121,7 @@ class NewMessageScreen extends BaseScreen { }; setAddRecipientByName = async (name: string, email: string) => { + await browser.pause(500); // stability fix for transition animation await (await this.addRecipientField).setValue(name); await ElementHelper.waitAndClick(await $(`~${email}`)); }; @@ -132,6 +165,12 @@ class NewMessageScreen extends BaseScreen { expect(name).toEqual(` ${recipient} `); } + deleteAddedRecipient = async (order: number, color: string) => { + const addedRecipientEl = await $(`~aid-to-${order}-${color}`); + await ElementHelper.waitAndClick(addedRecipientEl); + await driver.sendKeys(['\b']); // backspace + } + checkAddedAttachment = async (name: string) => { await (await this.deleteAttachmentButton).waitForDisplayed(); const label = await this.attachmentNameLabel; @@ -151,6 +190,27 @@ class NewMessageScreen extends BaseScreen { clickSendButton = async () => { await ElementHelper.waitAndClick(await this.sendButton); } + + clickSetPasswordButton = async () => { + await ElementHelper.waitAndClick(await this.setPasswordButton); + } + + clickCancelButton = async () => { + await ElementHelper.waitAndClick(await this.cancelButton); + } + + checkPasswordCell = async (text: string) => { + await ElementHelper.checkStaticText(await this.passwordCell, text); + } + + clickPasswordCell = async () => { + await ElementHelper.waitAndClick(await this.passwordCell); + } + + setMessagePassword = async (password: string) => { + await (await this.passwordTextField).setValue(password); + await this.clickSetPasswordButton(); + } } export default new NewMessageScreen(); diff --git a/appium/tests/screenobjects/splash.screen.ts b/appium/tests/screenobjects/splash.screen.ts index a59b5b3ce..2c8800322 100644 --- a/appium/tests/screenobjects/splash.screen.ts +++ b/appium/tests/screenobjects/splash.screen.ts @@ -99,8 +99,8 @@ class SplashScreen extends BaseScreen { } clickContinueBtn = async () => { - expect(await this.continueButton).toBeDisplayed(); - expect(await this.cancelButton).toBeDisplayed(); + // expect(await this.continueButton).toBeDisplayed(); + // expect(await this.cancelButton).toBeDisplayed(); await ElementHelper.waitAndClick(await this.continueButton); } diff --git a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 4b3356bc1..85cd96577 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -6,26 +6,43 @@ import { } from '../../../screenobjects/all-screens'; import { CommonData } from '../../../data'; -import BaseScreen from "../../../screenobjects/base.screen"; +import BaseScreen from '../../../screenobjects/base.screen'; describe('COMPOSE EMAIL: ', () => { - it('sending message to user without public key produces modal', async () => { + it('sending message to user without public key produces password modal', async () => { - const noPublicKeyRecipient = CommonData.recipientWithoutPublicKey.email; + const recipient = CommonData.recipientWithoutPublicKey.email; const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; - const noPublicKeyError = CommonData.errors.noPublicKey; + const emailPassword = CommonData.recipientWithoutPublicKey.password; + + const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage; + const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; + const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage; await SplashScreen.login(); await SetupKeyScreen.setPassPhrase(); await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(noPublicKeyRecipient, emailSubject, emailText); - await NewMessageScreen.checkFilledComposeEmailInfo(noPublicKeyRecipient, emailSubject, emailText); + await NewMessageScreen.composeEmail(recipient, emailSubject, emailText); + await NewMessageScreen.checkFilledComposeEmailInfo(recipient, emailSubject, emailText); + await NewMessageScreen.clickSendButton(); + await BaseScreen.checkModalMessage(passwordModalMessage); + await NewMessageScreen.clickCancelButton(); + await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); + + await NewMessageScreen.deleteAddedRecipient(0, 'gray'); + + await NewMessageScreen.setAddRecipient(recipient); await NewMessageScreen.clickSendButton(); + await BaseScreen.checkModalMessage(passwordModalMessage); + await NewMessageScreen.clickCancelButton(); + await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); - await BaseScreen.checkModalMessage(noPublicKeyError); + await NewMessageScreen.clickPasswordCell(); + await NewMessageScreen.setMessagePassword(emailPassword); + await NewMessageScreen.checkPasswordCell(addedPasswordMessage); }); });