From 11eff3c6f22d719a8f6da1a4ab207587dbae66a8 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 15 Dec 2021 16:41:36 +0200 Subject: [PATCH 01/18] #1221 add pwd param to composeEmail --- FlowCrypt.xcodeproj/project.pbxproj | 14 +++++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Compose/ComposeViewController.swift | 32 ++++++++++------ .../Compose/ComposeViewDecorator.swift | 26 ++++++------- FlowCrypt/Core/Core.swift | 3 +- FlowCrypt/Core/CoreTypes.swift | 1 + .../Backup Services/BackupService.swift | 3 +- .../ComposeMessageService.swift | 4 +- .../Cell Nodes/MessagePasswordCellNode.swift | 35 +++++++++++++++++ FlowCryptUI/Nodes/TextWithPaddingNode.swift | 38 +++++++++++++++++++ 10 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift create mode 100644 FlowCryptUI/Nodes/TextWithPaddingNode.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index b594fa678..6ea243659 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,6 +61,8 @@ 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 /* TextWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* TextWithPaddingNode.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 */; }; @@ -111,8 +112,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 */; }; @@ -455,7 +456,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 = ""; }; @@ -491,6 +491,8 @@ 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 /* TextWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithPaddingNode.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 = ""; }; @@ -534,8 +536,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 = ""; }; @@ -2007,6 +2009,7 @@ 5A39F433239EC61C001F4607 /* TitleCellNode.swift */, 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */, 9F82779D23737E3800E19C07 /* MessageTextSubjectNode.swift */, + 511D07E02769FBBA0050417B /* MessagePasswordCellNode.swift */, 5180CB96273724E9001FC7EF /* ThreadMessageInfoCellNode.swift */, 9F56BD3123438B5B00A7371A /* InboxCellNode.swift */, 9F56BD3523438B9D00A7371A /* TextCellNode.swift */, @@ -2034,6 +2037,7 @@ children = ( 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */, 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */, + 511D07E2276A2DF80050417B /* TextWithPaddingNode.swift */, 9FA1988F253C841F008C9CF2 /* TableViewController.swift */, 9F696292236091DD003712E1 /* SignInImageNode.swift */, 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */, @@ -2767,6 +2771,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 511D07E3276A2DF80050417B /* TextWithPaddingNode.swift in Sources */, 9F7ECCA7272C3FB4008A1770 /* TextImageNode.swift in Sources */, D27177452424D44200BDA9A9 /* ComposeButtonNode.swift in Sources */, D24FAFAB2520BFAE00BF46C5 /* CheckBoxNode.swift in Sources */, @@ -2789,6 +2794,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.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9f5ced7f3..0819df7bf 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "f483fa0a52f6d49897d133a827510a35e21183c1", - "version": "10.20.1" + "revision": "bdbbd57f411a0f4e72b359113dbc6d23fdf96680", + "version": "10.20.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 3b32618df..111447fc5 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -31,12 +31,12 @@ final class ComposeViewController: TableNodeViewController { case main, searchEmails([String]) } - private enum RecipientParts: Int, CaseIterable { + private enum RecipientPart: Int, CaseIterable { case recipient, recipientsInput, recipientDivider } - private enum ComposeParts: Int, CaseIterable { - case subject, subjectDivider, text + private enum ComposePart: Int, CaseIterable { + case subject, subjectDivider, password, text } private let appContext: AppContext @@ -63,6 +63,7 @@ final class ComposeViewController: TableNodeViewController { private var state: State = .main private var shouldEvaluateRecipientInput = true + private var shouldShowMessagePassword = true private weak var saveDraftTimer: Timer? private var composedLatestDraft: ComposedDraft? @@ -525,13 +526,13 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { switch (state, section) { case (.main, 0): - return RecipientParts.allCases.count + return RecipientPart.allCases.count case (.main, 1): - return ComposeParts.allCases.count + return ComposePart.allCases.count case (.main, 2): return contextToSend.attachments.count case (.searchEmails, 0): - return RecipientParts.allCases.count + return RecipientPart.allCases.count case let (.searchEmails(emails), 1): return emails.isNotEmpty ? emails.count : 1 case (.searchEmails, 2): @@ -548,17 +549,18 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch (self.state, indexPath.section) { case (_, 0): - guard let part = RecipientParts(rawValue: indexPath.row) else { return ASCellNode() } + 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 (.main, 1): - guard let composePart = ComposeParts(rawValue: indexPath.row) else { return ASCellNode() } + guard let composePart = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } switch composePart { case .subject: return self.subjectNode() case .text: return self.textNode() + case .password: return self.passwordNode() case .subjectDivider: return DividerCellNode() } case (.main, 2): @@ -597,7 +599,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { navigationController?.pushViewController(controller, animated: true ) } } - } + } } // MARK: - Nodes @@ -631,6 +633,12 @@ extension ComposeViewController { } } + private func passwordNode() -> ASCellNode { + MessagePasswordCellNode( + "Tap to add password for recipients who don't have encryption set up.".attributed(.regular(14), color: .white) + ) + } + private func textNode() -> ASCellNode { let styledQuote = decorator.styledQuote(with: input) let height = max(decorator.frame(for: styledQuote).height, 40) @@ -747,11 +755,11 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { private var textField: TextFieldNode? { - (node.nodeForRow(at: IndexPath(row: RecipientParts.recipientsInput.rawValue, section: 0)) as? TextFieldCellNode)?.textField + (node.nodeForRow(at: IndexPath(row: RecipientPart.recipientsInput.rawValue, section: 0)) as? TextFieldCellNode)?.textField } private var recipientsIndexPath: IndexPath { - IndexPath(row: RecipientParts.recipient.rawValue, section: 0) + IndexPath(row: RecipientPart.recipient.rawValue, section: 0) } private var recipients: [ComposeMessageRecipient] { @@ -760,7 +768,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() } diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index a5adcab72..fa20df858 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, @@ -101,7 +101,7 @@ struct ComposeViewDecorator { 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 +111,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 5b8f6773c..06cf0fedb 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) } diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 10ae43877..886958224 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -140,6 +140,7 @@ struct SendableMsg: Equatable { let atts: [Attachment] let pubKeys: [String]? let signingPrv: PrvKeyInfo? + let password: String? } struct MsgBlock: Decodable { 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/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index f7e048849..e83ddcf7e 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -17,6 +17,7 @@ struct ComposeMessageContext: Equatable { var message: String? var recipients: [ComposeMessageRecipient] = [] var subject: String? + var password: String? var attachments: [MessageAttachment] = [] } @@ -123,7 +124,8 @@ final class ComposeMessageService { replyToMimeMsg: replyToMimeMsg, atts: sendableAttachments, pubKeys: [myPubKey] + allRecipientPubs, - signingPrv: signingPrv + signingPrv: signingPrv, + password: contextToSend.password ) } diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift new file mode 100644 index 000000000..b2015aba7 --- /dev/null +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -0,0 +1,35 @@ +// +// 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 { + private let textNode: TextWithPaddingNode + + public init(_ text: NSAttributedString?) { + textNode = TextWithPaddingNode( + text: text, + insets: UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6), + backgroundColor: .main, + cornerRadius: 6 + ) + super.init() + + automaticallyManagesSubnodes = true + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + textNode.style.flexGrow = 1.0 + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), + child: textNode + ) + } +} diff --git a/FlowCryptUI/Nodes/TextWithPaddingNode.swift b/FlowCryptUI/Nodes/TextWithPaddingNode.swift new file mode 100644 index 000000000..8270aa833 --- /dev/null +++ b/FlowCryptUI/Nodes/TextWithPaddingNode.swift @@ -0,0 +1,38 @@ +// +// TextWithPaddingNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 15/12/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class TextWithPaddingNode: ASDisplayNode { + private let insets: UIEdgeInsets + private let textNode = ASTextNode2() + + public init( + text: NSAttributedString?, + insets: UIEdgeInsets, + backgroundColor: UIColor? = nil, + cornerRadius: CGFloat = 0 + ) { + self.insets = insets + + super.init() + + automaticallyManagesSubnodes = true + textNode.attributedText = text + + self.backgroundColor = backgroundColor + self.cornerRadius = cornerRadius + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + ASInsetLayoutSpec( + insets: insets, + child: textNode + ) + } +} From b1655079c803430df15eb6098d315e19fcb7e763 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 16 Dec 2021 17:22:37 +0200 Subject: [PATCH 02/18] #1221 update ui --- FlowCrypt.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Compose/ComposeViewController.swift | 84 ++++++++++++++----- .../Compose/ComposeViewDecorator.swift | 22 +++++ .../ComposeMessageService.swift | 9 ++ .../Resources/en.lproj/Localizable.strings | 5 ++ .../Cell Nodes/MessagePasswordCellNode.swift | 53 +++++++++--- ...Node.swift => ButtonWithPaddingNode.swift} | 13 +-- 8 files changed, 156 insertions(+), 42 deletions(-) rename FlowCryptUI/Nodes/{TextWithPaddingNode.swift => ButtonWithPaddingNode.swift} (71%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 6ea243659..c1bcc3ad7 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -62,7 +62,7 @@ 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 /* TextWithPaddingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D07E2276A2DF80050417B /* TextWithPaddingNode.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 */; }; @@ -492,7 +492,7 @@ 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 /* TextWithPaddingNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithPaddingNode.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 = ""; }; @@ -2037,7 +2037,7 @@ children = ( 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */, 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */, - 511D07E2276A2DF80050417B /* TextWithPaddingNode.swift */, + 511D07E2276A2DF80050417B /* ButtonWithPaddingNode.swift */, 9FA1988F253C841F008C9CF2 /* TableViewController.swift */, 9F696292236091DD003712E1 /* SignInImageNode.swift */, 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */, @@ -2771,7 +2771,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 511D07E3276A2DF80050417B /* TextWithPaddingNode.swift in Sources */, + 511D07E3276A2DF80050417B /* ButtonWithPaddingNode.swift in Sources */, 9F7ECCA7272C3FB4008A1770 /* TextImageNode.swift in Sources */, D27177452424D44200BDA9A9 /* ComposeButtonNode.swift in Sources */, D24FAFAB2520BFAE00BF46C5 /* CheckBoxNode.swift in Sources */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0819df7bf..9f5ced7f3 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -87,8 +87,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "bdbbd57f411a0f4e72b359113dbc6d23fdf96680", - "version": "10.20.0" + "revision": "f483fa0a52f6d49897d133a827510a35e21183c1", + "version": "10.20.1" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 111447fc5..2b54a2be1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -27,16 +27,16 @@ final class ComposeViewController: TableNodeViewController { static let endTypingCharacters = [",", " ", "\n", ";"] } - enum State { + enum State: Equatable { case main, searchEmails([String]) } private enum RecipientPart: Int, CaseIterable { - case recipient, recipientsInput, recipientDivider + case list, input, password, divider } private enum ComposePart: Int, CaseIterable { - case subject, subjectDivider, password, text + case subject, subjectDivider, text } private let appContext: AppContext @@ -63,7 +63,6 @@ final class ComposeViewController: TableNodeViewController { private var state: State = .main private var shouldEvaluateRecipientInput = true - private var shouldShowMessagePassword = true private weak var saveDraftTimer: Timer? private var composedLatestDraft: ComposedDraft? @@ -73,6 +72,19 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } + private var recipientParts: [RecipientPart] { + RecipientPart.allCases + // TODO: +// contextToSend.hasRecipientsWithoutPubKeys && state == .main +// ? RecipientPart.allCases +// : RecipientPart.allCases.filter { $0 != .password } + } + + private var hasPassword: Bool { + guard let password = contextToSend.password else { return false } + return password.isNotEmpty + } + init( appContext: AppContext, notificationCenter: NotificationCenter = .default, @@ -526,13 +538,13 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { switch (state, section) { case (.main, 0): - return RecipientPart.allCases.count + return recipientParts.count case (.main, 1): return ComposePart.allCases.count case (.main, 2): return contextToSend.attachments.count case (.searchEmails, 0): - return RecipientPart.allCases.count + return recipientParts.count case let (.searchEmails(emails), 1): return emails.isNotEmpty ? emails.count : 1 case (.searchEmails, 2): @@ -549,18 +561,17 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { switch (self.state, indexPath.section) { case (_, 0): - 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() + switch self.recipientParts[indexPath.row] { + case .divider: return DividerCellNode() + case .input: return self.recipientInput() + case .list: return self.recipientsNode() + case .password: return self.passwordNode() } case (.main, 1): - guard let composePart = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } - switch composePart { + guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } + switch part { case .subject: return self.subjectNode() case .text: return self.textNode() - case .password: return self.passwordNode() case .subjectDivider: return DividerCellNode() } case (.main, 2): @@ -634,8 +645,13 @@ extension ComposeViewController { } private func passwordNode() -> ASCellNode { - MessagePasswordCellNode( - "Tap to add password for recipients who don't have encryption set up.".attributed(.regular(14), color: .white) + let input = hasPassword + ? decorator.styledFilledPasswordInput() + : decorator.styledEmptyPasswordInput() + + return MessagePasswordCellNode( + input: input, + enterMessagePassword: { [weak self] in self?.updateMessagePassword() } ) } @@ -755,11 +771,11 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { private var textField: TextFieldNode? { - (node.nodeForRow(at: IndexPath(row: RecipientPart.recipientsInput.rawValue, section: 0)) as? TextFieldCellNode)?.textField + (node.nodeForRow(at: IndexPath(row: RecipientPart.input.rawValue, section: 0)) as? TextFieldCellNode)?.textField } private var recipientsIndexPath: IndexPath { - IndexPath(row: RecipientPart.recipient.rawValue, section: 0) + IndexPath(row: RecipientPart.list.rawValue, section: 0) } private var recipients: [ComposeMessageRecipient] { @@ -963,7 +979,7 @@ extension ComposeViewController { guard let recipientIndex = index else { return } contextToSend.recipients[recipientIndex].state = state - node.reloadRows(at: [recipientsIndexPath], with: .fade) + node.reloadSections(IndexSet(integer: 0), with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath) { @@ -1001,6 +1017,36 @@ extension ComposeViewController { } } } + + private func updateMessagePassword() { + Task { + contextToSend.password = await awaitMessagePasswordEntry() + } + } + + private func awaitMessagePasswordEntry() async -> String? { + return await withCheckedContinuation { (continuation: CheckedContinuation) in + let alert = UIAlertController( + title: "compose_password_modal_title".localized, + message: "compose_password_modal_message".localized, + preferredStyle: .alert + ) + + alert.addTextField { [weak self] textField in + textField.text = self?.contextToSend.password + textField.accessibilityLabel = "messagePasswordTextField" + } + + alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { _ in + return continuation.resume(returning: nil) + }) + alert.addAction(UIAlertAction(title: "set".localized, style: .default) { [weak alert] _ in + return continuation.resume(returning: alert?.textFields?[0].text) + }) + + self.present(alert, animated: true, completion: nil) + } + } } // MARK: - State Handling diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index fa20df858..147d58400 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -98,6 +98,28 @@ struct ComposeViewDecorator { return (text + message).attributed(.regular(17)) } + func styledEmptyPasswordInput() -> MessagePasswordCellNode.Input { + .init( + text: "compose_password_placeholder".localized.attributed( + .regular(14), + color: .warningColor + ), + color: .warningColor, + image: UIImage(systemName: "lock")?.tinted(.warningColor) + ) + } + + func styledFilledPasswordInput() -> MessagePasswordCellNode.Input { + .init( + text: "compose_password_set_message".localized.attributed( + .regular(14), + color: .main + ), + color: .main, + image: UIImage(systemName: "checkmark.circle")?.tinted(.main) + ) + } + 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 diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index e83ddcf7e..8e5605c31 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -21,6 +21,15 @@ struct ComposeMessageContext: Equatable { var attachments: [MessageAttachment] = [] } +extension ComposeMessageContext { + var hasRecipientsWithoutPubKeys: Bool { + recipients.first(where: { + if case .keyNotFound = $0.state { return true } + return false + }) != nil + } +} + struct ComposeMessageRecipient: Equatable { let email: String var state: RecipientState diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 3949bcd71..3df1f1061 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"; "compose_error" = "Could not compose message"; "compose_reply_successful" = "Reply successfully sent"; "compose_quote_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift index b2015aba7..2bd34ecc8 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -10,26 +10,57 @@ import AsyncDisplayKit import UIKit public final class MessagePasswordCellNode: CellNode { - private let textNode: TextWithPaddingNode - - public init(_ text: NSAttributedString?) { - textNode = TextWithPaddingNode( - text: text, - insets: UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6), - backgroundColor: .main, - cornerRadius: 6 - ) + 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 enterMessagePassword: (() -> Void)? + + public init(input: Input, + enterMessagePassword: (() -> Void)?) { + self.input = input + self.enterMessagePassword = enterMessagePassword + super.init() automaticallyManagesSubnodes = true + + setupButtonNode() + } + + private func setupButtonNode() { + 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 { - textNode.style.flexGrow = 1.0 + buttonNode.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) + buttonNode.borderColor = input.color.cgColor + buttonNode.borderWidth = 1 + buttonNode.cornerRadius = 6 + buttonNode.style.flexGrow = 1.0 return ASInsetLayoutSpec( insets: UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8), - child: textNode + child: buttonNode ) } + + @objc private func onButtonTap() { + enterMessagePassword?() + } } diff --git a/FlowCryptUI/Nodes/TextWithPaddingNode.swift b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift similarity index 71% rename from FlowCryptUI/Nodes/TextWithPaddingNode.swift rename to FlowCryptUI/Nodes/ButtonWithPaddingNode.swift index 8270aa833..43e90966a 100644 --- a/FlowCryptUI/Nodes/TextWithPaddingNode.swift +++ b/FlowCryptUI/Nodes/ButtonWithPaddingNode.swift @@ -1,5 +1,5 @@ // -// TextWithPaddingNode.swift +// ButtonWithPaddingNode.swift // FlowCryptUI // // Created by Roma Sosnovsky on 15/12/21 @@ -8,22 +8,23 @@ import AsyncDisplayKit -public final class TextWithPaddingNode: ASDisplayNode { +public final class ButtonWithPaddingNode: ASDisplayNode { private let insets: UIEdgeInsets - private let textNode = ASTextNode2() + private let buttonNode = ASTextNode2() public init( text: NSAttributedString?, insets: UIEdgeInsets, backgroundColor: UIColor? = nil, - cornerRadius: CGFloat = 0 + cornerRadius: CGFloat = 0, + action: (() -> Void)? ) { self.insets = insets super.init() automaticallyManagesSubnodes = true - textNode.attributedText = text + buttonNode.attributedText = text self.backgroundColor = backgroundColor self.cornerRadius = cornerRadius @@ -32,7 +33,7 @@ public final class TextWithPaddingNode: ASDisplayNode { public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, - child: textNode + child: buttonNode ) } } From 27838c492d5f9c74c81b870864daed2272874174 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 17 Dec 2021 16:20:34 +0200 Subject: [PATCH 03/18] #1221 update message validation --- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Compose/ComposeViewController.swift | 36 ++++++++------- FlowCrypt/Core/Core.swift | 2 +- .../ComposeMessageError.swift | 3 ++ .../ComposeMessageService.swift | 45 ++++++++++++++++--- .../Cell Nodes/MessagePasswordCellNode.swift | 33 +++++++++----- 6 files changed, 87 insertions(+), 36 deletions(-) diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9f5ced7f3..5426c63b4 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/google/GoogleSignIn-iOS", "state": { "branch": null, - "revision": "27bc94f5e1bd1f3b12ba5abb2230dd77dd143f1e", - "version": "6.0.2" + "revision": "60ca2bfd218ccb194a746a79b41d9d50eb7e3af0", + "version": "6.1.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 1b28ef850..f34d5e276 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -73,16 +73,9 @@ final class ComposeViewController: TableNodeViewController { } private var recipientParts: [RecipientPart] { - RecipientPart.allCases - // TODO: -// contextToSend.hasRecipientsWithoutPubKeys && state == .main -// ? RecipientPart.allCases -// : RecipientPart.allCases.filter { $0 != .password } - } - - private var hasPassword: Bool { - guard let password = contextToSend.password else { return false } - return password.isNotEmpty + contextToSend.hasRecipientsWithoutPubKeys && state == .main + ? RecipientPart.allCases + : RecipientPart.allCases.filter { $0 != .password } } init( @@ -518,7 +511,12 @@ 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) + switch error { + case MessageValidationError.needsMessagePassword: + self?.setMessagePassword() + default: + self?.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + } } } @@ -648,13 +646,13 @@ extension ComposeViewController { } private func passwordNode() -> ASCellNode { - let input = hasPassword + let input = contextToSend.hasPassword ? decorator.styledFilledPasswordInput() : decorator.styledEmptyPasswordInput() return MessagePasswordCellNode( input: input, - enterMessagePassword: { [weak self] in self?.updateMessagePassword() } + setMessagePassword: { [weak self] in self?.setMessagePassword() } ) } @@ -781,6 +779,10 @@ extension ComposeViewController { IndexPath(row: RecipientPart.list.rawValue, section: 0) } + private var passwordIndexPath: IndexPath { + IndexPath(row: RecipientPart.password.rawValue, section: 0) + } + private var recipients: [ComposeMessageRecipient] { contextToSend.recipients } @@ -1021,9 +1023,10 @@ extension ComposeViewController { } } - private func updateMessagePassword() { + private func setMessagePassword() { Task { contextToSend.password = await awaitMessagePasswordEntry() + node.reloadRows(at: [passwordIndexPath], with: .automatic) } } @@ -1036,12 +1039,13 @@ extension ComposeViewController { ) alert.addTextField { [weak self] textField in + textField.isSecureTextEntry = true textField.text = self?.contextToSend.password textField.accessibilityLabel = "messagePasswordTextField" } - alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { _ in - return continuation.resume(returning: nil) + alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in + return continuation.resume(returning: self?.contextToSend.password) }) alert.addAction(UIAlertAction(title: "set".localized, style: .default) { [weak alert] _ in return continuation.resume(returning: alert?.textFields?[0].text) diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 06cf0fedb..556a9a1bc 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -248,7 +248,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/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index c464947aa..ac9530439 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -14,6 +14,7 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable { case emptyMessage case missedPublicKey case noPubRecipients + case needsMessagePassword case revokedKeyRecipients case expiredKeyRecipients case invalidEmailRecipient @@ -31,6 +32,8 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable { return "compose_no_pub_sender".localized case .noPubRecipients: return "compose_recipient_no_pub".localized + case .needsMessagePassword: + return "" case .revokedKeyRecipients: return "compose_recipient_revoked".localized case .expiredKeyRecipients: diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 8e5605c31..a8f82ce79 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -21,6 +21,13 @@ struct ComposeMessageContext: Equatable { var attachments: [MessageAttachment] = [] } +extension ComposeMessageContext { + var hasPassword: Bool { + guard let password = password else { return false } + return password.isNotEmpty + } +} + extension ComposeMessageContext { var hasRecipientsWithoutPubKeys: Bool { recipients.first(where: { @@ -119,7 +126,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, + withMessagePassword: contextToSend.hasPassword + ) let replyToMimeMsg = input.replyToMime .flatMap { String(data: $0, encoding: .utf8) } @@ -132,37 +143,57 @@ final class ComposeMessageService { subject: subject, replyToMimeMsg: replyToMimeMsg, atts: sendableAttachments, - pubKeys: [myPubKey] + allRecipientPubs, + pubKeys: [myPubKey] + validPubKeys, signingPrv: signingPrv, password: contextToSend.password ) } - private func getPubKeys(for recipients: [ComposeMessageRecipient]) async throws -> [String] { + 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], withMessagePassword: Bool) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.first(where: { $0.keyState == keyState }) != nil } + + func hasRecipientsWithoutPubKey(withPasswordSupport: Bool) -> Bool { + recipients + .filter { $0.keyState == .empty } + .first(where: { + guard let domain = $0.email.recipientDomain else { return !withPasswordSupport } + let supportsPassword = domainsWithPasswordSupport.containsCaseInsensitive(domain) + return withPasswordSupport ? supportsPassword : !supportsPassword + }) != 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))") + + let domainsWithPasswordSupport = ["gmail.com", "flowcrypt.com"] // TODO: + + guard withMessagePassword || !hasRecipientsWithoutPubKey(withPasswordSupport: true) else { + throw MessageValidationError.needsMessagePassword + } + + guard !hasRecipientsWithoutPubKey(withPasswordSupport: false) 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) } diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift index 2bd34ecc8..9842140be 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -27,12 +27,12 @@ public final class MessagePasswordCellNode: CellNode { private let input: Input private let buttonNode = ASButtonNode() - private let enterMessagePassword: (() -> Void)? + private let setMessagePassword: (() -> Void)? public init(input: Input, - enterMessagePassword: (() -> Void)?) { + setMessagePassword: (() -> Void)?) { self.input = input - self.enterMessagePassword = enterMessagePassword + self.setMessagePassword = setMessagePassword super.init() @@ -42,25 +42,38 @@ public final class MessagePasswordCellNode: CellNode { } 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.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.contentEdgeInsets = UIEdgeInsets(top: 4, left: 8, bottom: 4, right: 8) - buttonNode.borderColor = input.color.cgColor - buttonNode.borderWidth = 1 - buttonNode.cornerRadius = 6 - buttonNode.style.flexGrow = 1.0 + 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: buttonNode + child: spec ) } @objc private func onButtonTap() { - enterMessagePassword?() + setMessagePassword?() } } From 51db16dce3aa6a5ec4a3f89e3aebeed5d2acdb7b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 17 Dec 2021 17:09:35 +0200 Subject: [PATCH 04/18] #1221 update tests --- .../ComposeMessageService.swift | 2 +- .../Core/FlowCryptCoreTests.swift | 12 +- .../Services/ComposeMessageServiceTests.swift | 143 +++++++++--------- 3 files changed, 84 insertions(+), 73 deletions(-) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index a8f82ce79..7a59b146a 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -177,7 +177,7 @@ final class ComposeMessageService { logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map(\.keyState))") - let domainsWithPasswordSupport = ["gmail.com", "flowcrypt.com"] // TODO: + let domainsWithPasswordSupport = ["flowcrypt.com"] guard withMessagePassword || !hasRecipientsWithoutPubKey(withPasswordSupport: true) else { throw MessageValidationError.needsMessagePassword diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 9020e69b1..4d306a96a 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..e48f0a00c 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 @@ -422,18 +382,64 @@ class ComposeMessageServiceTests: XCTestCase { } } - func testSuccessfulMessageValidation() async throws { - encryptedStorage.getKeypairsResult = [ - Keypair( - primaryFingerprint: "", - private: "", - public: "public key", - passphrase: nil, - source: "", - allFingerprints: [], - allLongids: [] + func testValidateMessageInputWithMessagePasswordSupport() async throws { + encryptedStorage.getKeypairsResult = [keypair] + contactsService.retrievePubKeysResult = { _ in return [] } + + let message = "some message" + let subject = "Some subject" + let password = "123" + let email = "some@gmail.com" + let recipient = ComposeMessageRecipient( + email: "robot@flowcrypt.com", + state: recipientIdleState + ) + + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: message, + recipients: [recipient], + subject: subject + ), + email: email, + signingPrv: nil ) - ] + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.needsMessagePassword) + } + + let result = try? await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: message, + recipients: [recipient], + subject: subject, + password: password + ), + email: email, + signingPrv: nil + ) + let expected = SendableMsg( + text: message, + to: [recipient.email], + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: nil, + atts: [], + pubKeys: ["public key"], + signingPrv: nil, + password: password) + + XCTAssertEqual(result, expected) + } + + func testSuccessfulMessageValidation() async throws { + encryptedStorage.getKeypairsResult = [keypair] recipients.enumerated().forEach { element, index in contactsService.retrievePubKeysResult = { recipient in ["pubKey"] @@ -470,7 +476,8 @@ class ComposeMessageServiceTests: XCTestCase { "pubKey", "pubKey" ], - signingPrv: nil) + signingPrv: nil, + password: nil) XCTAssertNotNil(result) XCTAssertEqual(result, expected) From 735fafd6706f20df81c98c65aacdc88c5474d876 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Sat, 18 Dec 2021 22:30:30 +0200 Subject: [PATCH 05/18] #1221 split ComposeMessageService --- FlowCrypt.xcodeproj/project.pbxproj | 12 ++++ .../ComposeMessageContext.swift | 31 +++++++++ .../ComposeMessageRecipient.swift | 20 ++++++ .../ComposeMessageService+State.swift | 41 +++++++++++ .../ComposeMessageService.swift | 69 +------------------ 5 files changed, 106 insertions(+), 67 deletions(-) create mode 100644 FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift create mode 100644 FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift create mode 100644 FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService+State.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index c1bcc3ad7..5a96ae97d 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -67,6 +67,9 @@ 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 */; }; @@ -496,6 +499,9 @@ 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 = ""; }; @@ -1375,7 +1381,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 = ""; @@ -2575,6 +2584,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 */, @@ -2596,6 +2606,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 */, @@ -2758,6 +2769,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 */, 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..8a5fa0095 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -0,0 +1,31 @@ +// +// 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 password: String? + var attachments: [MessageAttachment] = [] +} + +extension ComposeMessageContext { + var hasPassword: Bool { + guard let password = password else { return false } + return password.isNotEmpty + } + + var hasRecipientsWithoutPubKeys: Bool { + recipients.first(where: { + if case .keyNotFound = $0.state { return true } + return false + }) != nil + } +} 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..c0a2e82b9 --- /dev/null +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift @@ -0,0 +1,20 @@ +// +// 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 +} + +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 7a59b146a..7f2361d98 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -13,39 +13,6 @@ import FlowCryptCommon typealias RecipientState = RecipientEmailsCellNode.Input.State -struct ComposeMessageContext: Equatable { - var message: String? - var recipients: [ComposeMessageRecipient] = [] - var subject: String? - var password: String? - var attachments: [MessageAttachment] = [] -} - -extension ComposeMessageContext { - var hasPassword: Bool { - guard let password = password else { return false } - return password.isNotEmpty - } -} - -extension ComposeMessageContext { - var hasRecipientsWithoutPubKeys: Bool { - recipients.first(where: { - if case .keyNotFound = $0.state { return true } - return false - }) != nil - } -} - -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 } @@ -169,8 +136,8 @@ final class ComposeMessageService { .filter { $0.keyState == .empty } .first(where: { guard let domain = $0.email.recipientDomain else { return !withPasswordSupport } - let supportsPassword = domainsWithPasswordSupport.containsCaseInsensitive(domain) - return withPasswordSupport ? supportsPassword : !supportsPassword + let supportsPassword = domainsWithPasswordSupport.contains(domain) + return withPasswordSupport == supportsPassword }) != nil } @@ -236,35 +203,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 - } - } -} From 4556dd37f9362fa8e5b70103e98a039d92c64d5e Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 20 Dec 2021 17:47:25 +0200 Subject: [PATCH 06/18] #1221 update ui reloading --- .../Compose/ComposeViewController.swift | 97 ++++++++++--------- 1 file changed, 49 insertions(+), 48 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index f34d5e276..ea89cbf92 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 Section: Int, CaseIterable { + case recipient, password, compose, attachments + } + private enum RecipientPart: Int, CaseIterable { - case list, input, password, divider + case list, input } private enum ComposePart: Int, CaseIterable { - case subject, subjectDivider, text + case topDivider, subject, subjectDivider, text } private let appContext: AppContext @@ -52,10 +56,9 @@ final class ComposeViewController: TableNodeViewController { private let router: GlobalRouterType private let clientConfiguration: ClientConfiguration - private let search = PassthroughSubject() - private let email: String + private let search = PassthroughSubject() private var cancellable = Set() private var input: ComposeMessageInput @@ -72,12 +75,6 @@ final class ComposeViewController: TableNodeViewController { navigationController?.navigationBar.frame.maxY ?? 0 } - private var recipientParts: [RecipientPart] { - contextToSend.hasRecipientsWithoutPubKeys && state == .main - ? RecipientPart.allCases - : RecipientPart.allCases.filter { $0 != .password } - } - init( appContext: AppContext, notificationCenter: NotificationCenter = .default, @@ -329,7 +326,7 @@ 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 { @@ -338,7 +335,7 @@ extension ComposeViewController { } return $0 } - .sink { [weak self] in self?.searchEmail(with: $0) } + .sink(receiveValue: { [weak self] in self?.searchEmail(with: $0) }) .store(in: &cancellable) } } @@ -533,19 +530,19 @@ 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.count - case (.main, 1): + case (_, Section.recipient.rawValue): + return RecipientPart.allCases.count + case (.main, Section.password.rawValue): + return contextToSend.hasRecipientsWithoutPubKeys ? 1 : 0 + case (.main, Section.compose.rawValue): return ComposePart.allCases.count - case (.main, 2): + case (.main, Section.attachments.rawValue): return contextToSend.attachments.count - case (.searchEmails, 0): - return recipientParts.count case let (.searchEmails(emails), 1): return emails.isNotEmpty ? emails.count : 1 case (.searchEmails, 2): @@ -561,21 +558,22 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { guard let self = self else { return ASCellNode() } switch (self.state, indexPath.section) { - case (_, 0): - switch self.recipientParts[indexPath.row] { - case .divider: return DividerCellNode() + case (_, Section.recipient.rawValue): + guard let part = RecipientPart(rawValue: indexPath.row) else { return ASCellNode() } + switch part { case .input: return self.recipientInput() case .list: return self.recipientsNode() - case .password: return self.passwordNode() } - case (.main, 1): + case (.main, Section.password.rawValue): + return self.passwordNode() + 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() } @@ -602,14 +600,12 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { 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 ) } } } @@ -745,7 +741,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) } ) } @@ -772,15 +768,15 @@ extension ComposeViewController { // MARK: - Recipients Input extension ComposeViewController { private var textField: TextFieldNode? { - (node.nodeForRow(at: IndexPath(row: RecipientPart.input.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: RecipientPart.list.rawValue, section: 0) - } - - private var passwordIndexPath: IndexPath { - IndexPath(row: RecipientPart.password.rawValue, section: 0) + IndexPath(row: RecipientPart.list.rawValue, section: Section.recipient.rawValue) } private var recipients: [ComposeMessageRecipient] { @@ -888,6 +884,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() @@ -984,7 +981,9 @@ extension ComposeViewController { guard let recipientIndex = index else { return } contextToSend.recipients[recipientIndex].state = state - node.reloadSections(IndexSet(integer: 0), with: .automatic) + + node.reloadSections([Section.password.rawValue], with: .automatic) + node.reloadRows(at: [recipientsIndexPath], with: .automatic) } private func handleRecipientSelection(with indexPath: IndexPath) { @@ -998,7 +997,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() } @@ -1026,7 +1026,7 @@ extension ComposeViewController { private func setMessagePassword() { Task { contextToSend.password = await awaitMessagePasswordEntry() - node.reloadRows(at: [passwordIndexPath], with: .automatic) + node.reloadSections([Section.password.rawValue], with: .automatic) } } @@ -1065,7 +1065,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) } } } @@ -1080,7 +1081,7 @@ extension ComposeViewController: UIDocumentPickerDelegate { return } appendAttachmentIfAllowed(attachment) - node.reloadSections(IndexSet(integer: 2), with: .automatic) + node.reloadSections([Section.attachments.rawValue], with: .automatic) } } @@ -1137,7 +1138,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { } appendAttachmentIfAllowed(composeMessageAttachment) - node.reloadSections(IndexSet(integer: 2), with: .automatic) + node.reloadSections([Section.attachments.rawValue], with: .automatic) } } @@ -1160,7 +1161,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) { From 2d2d5ca8f4b56b3fd2e9144222fcd9ce4212590b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 21 Dec 2021 23:56:34 +0200 Subject: [PATCH 07/18] #1221 update ui test --- .../Compose/ComposeViewController.swift | 2 +- .../Cell Nodes/MessagePasswordCellNode.swift | 1 + appium/tests/data/index.ts | 5 +- .../tests/screenobjects/new-message.screen.ts | 57 +++++++++++++++++++ appium/tests/screenobjects/splash.screen.ts | 4 +- ...ndEmailToRecipientWithoutPublicKey.spec.ts | 12 +++- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index ea89cbf92..7422970c2 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -1041,7 +1041,7 @@ extension ComposeViewController { alert.addTextField { [weak self] textField in textField.isSecureTextEntry = true textField.text = self?.contextToSend.password - textField.accessibilityLabel = "messagePasswordTextField" + textField.accessibilityLabel = "aid-message-password-textfield" } alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in diff --git a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift index 9842140be..5cbc3c043 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -37,6 +37,7 @@ public final class MessagePasswordCellNode: CellNode { super.init() automaticallyManagesSubnodes = true + accessibilityIdentifier = "aid-message-password-cell" setupButtonNode() } diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 1998eed43..f3e1a1877 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -73,7 +73,10 @@ export const CommonData = { signatureBadgeText: 'not signed' }, 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`, + passwordAddedMessage: 'Web portal password added', }, recipientWithExpiredPublicKey: { email: 'expired@flowcrypt.com' diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index f1046883a..e3fc4613f 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,6 +69,30 @@ 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); @@ -151,6 +181,33 @@ 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); + } + + checkModalText = async (message: string) => { + await ElementHelper.waitElementVisible(await this.currentModal); + const alertText = await driver.getAlertText(); + expect(alertText).toEqual(message); + } + + 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 3c660b21f..2b5d649f6 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -6,7 +6,6 @@ import { } from '../../../screenobjects/all-screens'; import { CommonData } from '../../../data'; -import BaseScreen from "../../../screenobjects/base.screen"; describe('COMPOSE EMAIL: ', () => { @@ -15,7 +14,8 @@ describe('COMPOSE EMAIL: ', () => { const noPublicKeyRecipient = CommonData.recipientWithoutPublicKey.email; const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; - const noPublicKeyError = CommonData.errors.noPublicKey; + const emailPassword = CommonData.recipientWithoutPublicKey.password; + const modalMessage = CommonData.recipientWithoutPublicKey.modalMessage; await SplashScreen.login(); await SetupKeyScreen.setPassPhrase(); @@ -26,6 +26,12 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.checkFilledComposeEmailInfo(noPublicKeyRecipient, emailSubject, emailText); await NewMessageScreen.clickSendButton(); - await BaseScreen.checkErrorModal(noPublicKeyError); + await NewMessageScreen.checkModalText(modalMessage); + await NewMessageScreen.clickCancelButton(); + await NewMessageScreen.checkPasswordCell("Tap to add password for recipients who don't have encryption set up."); + + await NewMessageScreen.clickPasswordCell(); + await NewMessageScreen.setMessagePassword(emailPassword); + await NewMessageScreen.checkPasswordCell("Web portal password added"); }); }); From a1878ace49e56823b372eae3fc16f8dd44e2f456 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 22 Dec 2021 16:19:11 +0200 Subject: [PATCH 08/18] #1221 update tests --- FlowCrypt/Resources/en.lproj/Localizable.strings | 2 +- FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift | 2 +- appium/tests/data/index.ts | 5 +++-- .../SendEmailToRecipientWithoutPublicKey.spec.ts | 6 ++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 3df1f1061..5bfbaffb8 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -82,7 +82,7 @@ "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"; +"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/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift index 5cbc3c043..7a3d32c08 100644 --- a/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift +++ b/FlowCryptUI/Cell Nodes/MessagePasswordCellNode.swift @@ -37,7 +37,6 @@ public final class MessagePasswordCellNode: CellNode { super.init() automaticallyManagesSubnodes = true - accessibilityIdentifier = "aid-message-password-cell" setupButtonNode() } @@ -48,6 +47,7 @@ public final class MessagePasswordCellNode: CellNode { 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) diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index f3e1a1877..ec185f85f 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -75,8 +75,9 @@ export const CommonData = { recipientWithoutPublicKey: { 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`, - passwordAddedMessage: 'Web portal password added', + 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/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 2b5d649f6..9c95054dd 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -16,6 +16,8 @@ describe('COMPOSE EMAIL: ', () => { const emailText = CommonData.simpleEmail.message; const emailPassword = CommonData.recipientWithoutPublicKey.password; const modalMessage = CommonData.recipientWithoutPublicKey.modalMessage; + const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; + const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage; await SplashScreen.login(); await SetupKeyScreen.setPassPhrase(); @@ -28,10 +30,10 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.checkModalText(modalMessage); await NewMessageScreen.clickCancelButton(); - await NewMessageScreen.checkPasswordCell("Tap to add password for recipients who don't have encryption set up."); + await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); await NewMessageScreen.clickPasswordCell(); await NewMessageScreen.setMessagePassword(emailPassword); - await NewMessageScreen.checkPasswordCell("Web portal password added"); + await NewMessageScreen.checkPasswordCell(addedPasswordMessage); }); }); From 928156c2fa157a2e24a88e1068052682a9423af8 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 23 Dec 2021 09:35:50 +0200 Subject: [PATCH 09/18] #1221 add check for domain password support --- .../Compose/ComposeViewController.swift | 2 +- FlowCrypt/Extensions/Error+Extension.swift | 2 +- .../ComposeMessageContext.swift | 17 +++++++--- appium/tests/data/index.ts | 33 ++++++++++--------- ...ndEmailToRecipientWithoutPublicKey.spec.ts | 20 ++++++++--- 5 files changed, 46 insertions(+), 28 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 7422970c2..0236557f1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -538,7 +538,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (_, Section.recipient.rawValue): return RecipientPart.allCases.count case (.main, Section.password.rawValue): - return contextToSend.hasRecipientsWithoutPubKeys ? 1 : 0 + return contextToSend.hasRecipientsWithoutPubKey(withPasswordSupport: true) ? 1 : 0 case (.main, Section.compose.rawValue): return ComposePart.allCases.count case (.main, Section.attachments.rawValue): 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/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 8a5fa0095..70fef8d8b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -22,10 +22,17 @@ extension ComposeMessageContext { return password.isNotEmpty } - var hasRecipientsWithoutPubKeys: Bool { - recipients.first(where: { - if case .keyNotFound = $0.state { return true } - return false - }) != nil + func hasRecipientsWithoutPubKey(withPasswordSupport: Bool) -> Bool { + recipients + .filter { + if case .keyNotFound = $0.state { return true } + return false + } + .first(where: { + guard let domain = $0.email.recipientDomain else { return !withPasswordSupport } + let domainsWithPasswordSupport = ["flowcrypt.com"] + let supportsPassword = domainsWithPasswordSupport.contains(domain) + return withPasswordSupport == supportsPassword + }) != nil } } diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index c2e0f3091..fa0ac2a92 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -75,7 +75,8 @@ export const CommonData = { signatureBadgeText: 'not signed' }, recipientWithoutPublicKey: { - email: 'no.publickey@flowcrypt.com', + emailWithoutPasswordSupport: 'some@gmail.com', + emailWithPasswordSupport: '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.', @@ -88,24 +89,24 @@ export const CommonData = { email: 'revoked@flowcrypt.com' }, errors: { - noPublicKey: 'Could not compose message\n' + - '\n' + - 'One or more of your recipients are missing a public key (marked in gray).\n' + - '\n' + - 'Please ask them to share it with you, or ask them to also set up FlowCrypt.', + noPublicKey: 'Error\nCould not compose message\n' + + '\n' + + 'One or more of your recipients are missing a public key (marked in gray).\n' + + '\n' + + 'Please ask them to share it with you, or ask them to also set up FlowCrypt.', wrongPassPhrase: 'Could not compose message\n' + - '\n' + - 'This pass phrase did not match your signing private key', + '\n' + + 'This pass phrase did not match your signing private key', expiredPublicKey: 'Could not compose message\n' + - '\n' + - 'One or more of your recipients have expired public keys (marked in orange).\n' + - '\n' + - 'Please ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.', + '\n' + + 'One or more of your recipients have expired public keys (marked in orange).\n' + + '\n' + + 'Please ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.', revokedPublicKey: 'Could not compose message\n' + - '\n' + - 'One or more of your recipients have revoked public keys (marked in red).\n' + - '\n' + - 'Please ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.' + '\n' + + 'One or more of your recipients have revoked public keys (marked in red).\n' + + '\n' + + 'Please ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.' }, decryptErrorBadge: { badgeText: 'decrypt error' diff --git a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 9c95054dd..9b5992d66 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -6,16 +6,19 @@ import { } from '../../../screenobjects/all-screens'; import { CommonData } from '../../../data'; +import BaseScreen from '../../../screenobjects/base.screen'; describe('COMPOSE EMAIL: ', () => { it('sending message to user without public key produces modal', async () => { - const noPublicKeyRecipient = CommonData.recipientWithoutPublicKey.email; + const recipientWithPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithPasswordSupport; + const recipientWithoutPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithoutPasswordSupport; const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; const emailPassword = CommonData.recipientWithoutPublicKey.password; - const modalMessage = CommonData.recipientWithoutPublicKey.modalMessage; + const noPubKeyErrorMessage = CommonData.errors.noPublicKey; + const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage; const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage; @@ -24,11 +27,18 @@ describe('COMPOSE EMAIL: ', () => { await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(noPublicKeyRecipient, emailSubject, emailText); - await NewMessageScreen.checkFilledComposeEmailInfo(noPublicKeyRecipient, emailSubject, emailText); + await NewMessageScreen.composeEmail(recipientWithoutPasswordSupport, emailSubject, emailText); + await NewMessageScreen.checkFilledComposeEmailInfo(recipientWithoutPasswordSupport, emailSubject, emailText); await NewMessageScreen.clickSendButton(); + await NewMessageScreen.checkModalText(noPubKeyErrorMessage); + await BaseScreen.clickOkButtonOnError(); + await NewMessageScreen.clickBackButton(); - await NewMessageScreen.checkModalText(modalMessage); + await MailFolderScreen.clickCreateEmail(); + await NewMessageScreen.composeEmail(recipientWithPasswordSupport, emailSubject, emailText); + await NewMessageScreen.checkFilledComposeEmailInfo(recipientWithPasswordSupport, emailSubject, emailText); + await NewMessageScreen.clickSendButton(); + await NewMessageScreen.checkModalText(passwordModalMessage); await NewMessageScreen.clickCancelButton(); await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); From 78dc1d34394d43e707eced0faf94bec1f568cea0 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 23 Dec 2021 15:11:39 +0200 Subject: [PATCH 10/18] #1221 improve search handling --- .../Compose/ComposeViewController.swift | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 0236557f1..fea892e5c 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -328,14 +328,14 @@ extension ComposeViewController { search .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(receiveValue: { [weak self] in self?.searchEmail(with: $0) }) + .sink(receiveValue: { [weak self] in + guard $0.isNotEmpty else { return } + self?.searchEmail(with: $0) + }) .store(in: &cancellable) } } @@ -544,7 +544,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, Section.attachments.rawValue): return contextToSend.attachments.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: @@ -579,8 +579,9 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } 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: @@ -861,6 +862,7 @@ extension ComposeViewController { // reset textfield textField?.reset() node.view.keyboardDismissMode = .interactive + search.send("") updateState(with: .main) } @@ -892,12 +894,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() { From 30fb7ae4bed1a828f0cbadf97ecd87d974196f56 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 23 Dec 2021 15:40:49 +0200 Subject: [PATCH 11/18] #1221 code updates --- .../Controllers/Compose/ComposeViewController.swift | 6 +++--- .../Controllers/Compose/ComposeViewDecorator.swift | 10 +++++----- .../SendEmailToRecipientWithoutPublicKey.spec.ts | 2 ++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index fea892e5c..55fe3d747 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -27,7 +27,7 @@ final class ComposeViewController: TableNodeViewController { static let endTypingCharacters = [",", " ", "\n", ";"] } - enum State: Equatable { + enum State { case main, searchEmails([String]) } @@ -1022,12 +1022,12 @@ extension ComposeViewController { private func setMessagePassword() { Task { - contextToSend.password = await awaitMessagePasswordEntry() + contextToSend.password = await enterMessagePassword() node.reloadSections([Section.password.rawValue], with: .automatic) } } - private func awaitMessagePasswordEntry() async -> String? { + private func enterMessagePassword() async -> String? { return await withCheckedContinuation { (continuation: CheckedContinuation) in let alert = UIAlertController( title: "compose_password_modal_title".localized, diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index 147d58400..fdde12319 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -134,21 +134,21 @@ struct ComposeViewDecorator { extension UIColor { static var titleNodeBackgroundColorSelected: UIColor { colorFor( - darkStyle: .lightGray, - lightStyle: .black.withAlphaComponent(0.1) + darkStyle: lightGray, + lightStyle: black.withAlphaComponent(0.1) ) } static var titleNodeBackgroundColor: UIColor { colorFor( - darkStyle: .darkGray.withAlphaComponent(0.5), - lightStyle: .white.withAlphaComponent(0.9) + darkStyle: darkGray.withAlphaComponent(0.5), + lightStyle: white.withAlphaComponent(0.9) ) } static var borderColorSelected: UIColor { colorFor( - darkStyle: .white.withAlphaComponent(0.5), + darkStyle: white.withAlphaComponent(0.5), lightStyle: black.withAlphaComponent(0.4) ) } diff --git a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 2d8ef4369..6a3997423 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -14,9 +14,11 @@ describe('COMPOSE EMAIL: ', () => { const recipientWithPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithPasswordSupport; const recipientWithoutPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithoutPasswordSupport; + const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; const emailPassword = CommonData.recipientWithoutPublicKey.password; + const noPubKeyErrorMessage = CommonData.errors.noPublicKey; const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage; const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; From ad03900b017c9b0a91a5f4cc3a0bede859b8cb0b Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 24 Dec 2021 13:50:56 +0200 Subject: [PATCH 12/18] #1221 pr comments updates --- .../Compose/ComposeViewController.swift | 34 ++++++----- .../Compose/ComposeViewDecorator.swift | 32 ++++++----- .../EnterpriseServerApi.swift | 4 +- .../ClientConfiguration.swift | 6 +- .../ComposeMessageContext.swift | 24 +++----- .../ComposeMessageError.swift | 3 - .../ComposeMessageService.swift | 31 ++++------ .../Remote Pub Key Services/WkdApi.swift | 2 +- .../Services/ComposeMessageServiceTests.swift | 56 ------------------- .../Extensions/StringExtensions.swift | 21 ++----- Gemfile.lock | 12 ++-- appium/package.json | 4 +- appium/tests/data/index.ts | 3 +- ...ndEmailToRecipientWithoutPublicKey.spec.ts | 19 ++----- 14 files changed, 80 insertions(+), 171 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 55fe3d747..89a08f221 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -57,6 +57,11 @@ final class ComposeViewController: TableNodeViewController { private let clientConfiguration: ClientConfiguration private let email: String + private var isMessagePasswordSupported: Bool { + guard let domain = email.emailDomain else { return false } + let senderDomainsWithMessagePasswordSupport = ["flowcrypt.com"] + return senderDomainsWithMessagePasswordSupport.contains(domain) + } private let search = PassthroughSubject() private var cancellable = Set() @@ -508,11 +513,12 @@ extension ComposeViewController { let hideSpinnerAnimationDuration: TimeInterval = 1 DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in - switch error { - case MessageValidationError.needsMessagePassword: - self?.setMessagePassword() - default: - self?.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) + guard let self = self else { return } + + if case MessageValidationError.missedPublicKey = error, self.isMessagePasswordSupported { + self.setMessagePassword() + } else { + self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) } } } @@ -538,7 +544,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (_, Section.recipient.rawValue): return RecipientPart.allCases.count case (.main, Section.password.rawValue): - return contextToSend.hasRecipientsWithoutPubKey(withPasswordSupport: true) ? 1 : 0 + return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 case (.main, Section.compose.rawValue): return ComposePart.allCases.count case (.main, Section.attachments.rawValue): @@ -565,7 +571,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case .list: return self.recipientsNode() } case (.main, Section.password.rawValue): - return self.passwordNode() + return self.messagePasswordNode() case (.main, Section.compose.rawValue): guard let part = ComposePart(rawValue: indexPath.row) else { return ASCellNode() } switch part { @@ -642,10 +648,10 @@ extension ComposeViewController { } } - private func passwordNode() -> ASCellNode { - let input = contextToSend.hasPassword - ? decorator.styledFilledPasswordInput() - : decorator.styledEmptyPasswordInput() + private func messagePasswordNode() -> ASCellNode { + let input = contextToSend.hasMessagePassword + ? decorator.styledFilledMessagePasswordInput() + : decorator.styledEmptyMessagePasswordInput() return MessagePasswordCellNode( input: input, @@ -1022,7 +1028,7 @@ extension ComposeViewController { private func setMessagePassword() { Task { - contextToSend.password = await enterMessagePassword() + contextToSend.messagePassword = await enterMessagePassword() node.reloadSections([Section.password.rawValue], with: .automatic) } } @@ -1037,12 +1043,12 @@ extension ComposeViewController { alert.addTextField { [weak self] textField in textField.isSecureTextEntry = true - textField.text = self?.contextToSend.password + textField.text = self?.contextToSend.messagePassword textField.accessibilityLabel = "aid-message-password-textfield" } alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in - return continuation.resume(returning: self?.contextToSend.password) + return continuation.resume(returning: self?.contextToSend.messagePassword) }) alert.addAction(UIAlertAction(title: "set".localized, style: .default) { [weak alert] _ in return continuation.resume(returning: alert?.textFields?[0].text) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift index fdde12319..3bacc9c54 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewDecorator.swift @@ -98,25 +98,29 @@ struct ComposeViewDecorator { return (text + message).attributed(.regular(17)) } - func styledEmptyPasswordInput() -> MessagePasswordCellNode.Input { - .init( - text: "compose_password_placeholder".localized.attributed( - .regular(14), - color: .warningColor - ), + func styledEmptyMessagePasswordInput() -> MessagePasswordCellNode.Input { + messagePasswordInput( + text: "compose_password_placeholder".localized, color: .warningColor, - image: UIImage(systemName: "lock")?.tinted(.warningColor) + imageName: "lock" ) } - func styledFilledPasswordInput() -> MessagePasswordCellNode.Input { - .init( - text: "compose_password_set_message".localized.attributed( - .regular(14), - color: .main - ), + func styledFilledMessagePasswordInput() -> MessagePasswordCellNode.Input { + messagePasswordInput( + text: "compose_password_set_message".localized, color: .main, - image: UIImage(systemName: "checkmark.circle")?.tinted(.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) ) } diff --git a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift index b5b90d7b4..055b29477 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.emailDomain, !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.emailDomain else { throw EnterpriseServerApiError.emailFormat } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift index fcd5ef061..020320d83 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.emailDomain 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 index 70fef8d8b..68d162689 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -12,27 +12,19 @@ struct ComposeMessageContext: Equatable { var message: String? var recipients: [ComposeMessageRecipient] = [] var subject: String? - var password: String? + var messagePassword: String? var attachments: [MessageAttachment] = [] } extension ComposeMessageContext { - var hasPassword: Bool { - guard let password = password else { return false } - return password.isNotEmpty + var hasMessagePassword: Bool { + (messagePassword ?? "").isNotEmpty } - func hasRecipientsWithoutPubKey(withPasswordSupport: Bool) -> Bool { - recipients - .filter { - if case .keyNotFound = $0.state { return true } - return false - } - .first(where: { - guard let domain = $0.email.recipientDomain else { return !withPasswordSupport } - let domainsWithPasswordSupport = ["flowcrypt.com"] - let supportsPassword = domainsWithPasswordSupport.contains(domain) - return withPasswordSupport == supportsPassword - }) != nil + var hasRecipientsWithoutPubKey: Bool { + recipients.first { + if case .keyNotFound = $0.state { return true } + return false + } != nil } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift index ac9530439..c464947aa 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageError.swift @@ -14,7 +14,6 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable { case emptyMessage case missedPublicKey case noPubRecipients - case needsMessagePassword case revokedKeyRecipients case expiredKeyRecipients case invalidEmailRecipient @@ -32,8 +31,6 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable { return "compose_no_pub_sender".localized case .noPubRecipients: return "compose_recipient_no_pub".localized - case .needsMessagePassword: - return "" case .revokedKeyRecipients: return "compose_recipient_revoked".localized case .expiredKeyRecipients: diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 7f2361d98..8f33123a7 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -96,7 +96,7 @@ final class ComposeMessageService { let recipientsWithPubKeys = try await getRecipientKeys(for: recipients) let validPubKeys = try validate( recipients: recipientsWithPubKeys, - withMessagePassword: contextToSend.hasPassword + hasMessagePassword: contextToSend.hasMessagePassword ) let replyToMimeMsg = input.replyToMime .flatMap { String(data: $0, encoding: .utf8) } @@ -112,10 +112,16 @@ final class ComposeMessageService { atts: sendableAttachments, pubKeys: [myPubKey] + validPubKeys, signingPrv: signingPrv, - password: contextToSend.password + password: contextToSend.messagePassword ) } + private func isMessagePasswordSupported(for email: String) -> Bool { + guard let senderDomain = email.emailDomain 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 { @@ -126,31 +132,16 @@ final class ComposeMessageService { return recipientsWithKeys } - private func validate(recipients: [RecipientWithSortedPubKeys], withMessagePassword: Bool) throws -> [String] { + private func validate(recipients: [RecipientWithSortedPubKeys], + hasMessagePassword: Bool) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.first(where: { $0.keyState == keyState }) != nil } - func hasRecipientsWithoutPubKey(withPasswordSupport: Bool) -> Bool { - recipients - .filter { $0.keyState == .empty } - .first(where: { - guard let domain = $0.email.recipientDomain else { return !withPasswordSupport } - let supportsPassword = domainsWithPasswordSupport.contains(domain) - return withPasswordSupport == supportsPassword - }) != nil - } - logger.logDebug("validate recipients: \(recipients)") logger.logDebug("validate recipient keyStates: \(recipients.map(\.keyState))") - let domainsWithPasswordSupport = ["flowcrypt.com"] - - guard withMessagePassword || !hasRecipientsWithoutPubKey(withPasswordSupport: true) else { - throw MessageValidationError.needsMessagePassword - } - - guard !hasRecipientsWithoutPubKey(withPasswordSupport: false) else { + guard hasMessagePassword || !contains(keyState: .empty) else { throw MessageValidationError.noPubRecipients } diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift index b228c22f7..b3e62602c 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 - !EnterpriseServerApi.publicEmailProviderDomains.contains(email.recipientDomain ?? ""), + !EnterpriseServerApi.publicEmailProviderDomains.contains(email.emailDomain ?? ""), let advancedUrl = urlConstructor.construct(from: email, method: .advanced), let directUrl = urlConstructor.construct(from: email, method: .direct) else { diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index e48f0a00c..59d30cfb8 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -382,62 +382,6 @@ class ComposeMessageServiceTests: XCTestCase { } } - func testValidateMessageInputWithMessagePasswordSupport() async throws { - encryptedStorage.getKeypairsResult = [keypair] - contactsService.retrievePubKeysResult = { _ in return [] } - - let message = "some message" - let subject = "Some subject" - let password = "123" - let email = "some@gmail.com" - let recipient = ComposeMessageRecipient( - email: "robot@flowcrypt.com", - state: recipientIdleState - ) - - do { - _ = try await sut.validateAndProduceSendableMsg( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: message, - recipients: [recipient], - subject: subject - ), - email: email, - signingPrv: nil - ) - XCTFail("expected to throw above") - } catch { - XCTAssertEqual(error as? MessageValidationError, MessageValidationError.needsMessagePassword) - } - - let result = try? await sut.validateAndProduceSendableMsg( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: message, - recipients: [recipient], - subject: subject, - password: password - ), - email: email, - signingPrv: nil - ) - let expected = SendableMsg( - text: message, - to: [recipient.email], - cc: [], - bcc: [], - from: email, - subject: subject, - replyToMimeMsg: nil, - atts: [], - pubKeys: ["public key"], - signingPrv: nil, - password: password) - - XCTAssertEqual(result, expected) - } - func testSuccessfulMessageValidation() async throws { encryptedStorage.getKeypairsResult = [keypair] recipients.enumerated().forEach { element, index in diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index f1e887912..64f5b0072 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -61,27 +61,14 @@ public extension NSAttributedString { // MARK: Email parsing public extension String { - var userAndRecipientDomain: (user: String, domain: String)? { + var email: (username: String?, domain: String?) { let parts = self.split(separator: "@") if parts.count != 2 { - return nil + return (nil, 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]) - } + var emailUsername: String? { email.username } + var emailDomain: String? { email.domain } } 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 b5e61dedb..6cd7329f6 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -81,8 +81,7 @@ export const CommonData = { firstAttachmentName: 'Screenshot_20180422_125217.png.asc' }, recipientWithoutPublicKey: { - emailWithoutPasswordSupport: 'some@gmail.com', - emailWithPasswordSupport: '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.', diff --git a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 6a3997423..3b509d3ba 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -10,16 +10,13 @@ import BaseScreen from '../../../screenobjects/base.screen'; describe('COMPOSE EMAIL: ', () => { - it('sending message to user without public key produces modal', async () => { - - const recipientWithPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithPasswordSupport; - const recipientWithoutPasswordSupport = CommonData.recipientWithoutPublicKey.emailWithoutPasswordSupport; + it('sending message to user without public key produces password modal', async () => { + const recipient = CommonData.recipientWithoutPublicKey.email; const emailSubject = CommonData.simpleEmail.subject; const emailText = CommonData.simpleEmail.message; const emailPassword = CommonData.recipientWithoutPublicKey.password; - const noPubKeyErrorMessage = CommonData.errors.noPublicKey; const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage; const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage; @@ -29,16 +26,8 @@ describe('COMPOSE EMAIL: ', () => { await MailFolderScreen.checkInboxScreen(); await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipientWithoutPasswordSupport, emailSubject, emailText); - await NewMessageScreen.checkFilledComposeEmailInfo(recipientWithoutPasswordSupport, emailSubject, emailText); - await NewMessageScreen.clickSendButton(); - await BaseScreen.checkModalMessage(noPubKeyErrorMessage); - await BaseScreen.clickOkButtonOnError(); - await NewMessageScreen.clickBackButton(); - - await MailFolderScreen.clickCreateEmail(); - await NewMessageScreen.composeEmail(recipientWithPasswordSupport, emailSubject, emailText); - await NewMessageScreen.checkFilledComposeEmailInfo(recipientWithPasswordSupport, emailSubject, emailText); + await NewMessageScreen.composeEmail(recipient, emailSubject, emailText); + await NewMessageScreen.checkFilledComposeEmailInfo(recipient, emailSubject, emailText); await NewMessageScreen.clickSendButton(); await BaseScreen.checkModalMessage(passwordModalMessage); await NewMessageScreen.clickCancelButton(); From e83ed1a05b64fbd1d156573bfcace7b5ba617632 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 24 Dec 2021 14:14:41 +0200 Subject: [PATCH 13/18] #1221 update emailParts extension --- .../Controllers/Compose/ComposeViewController.swift | 2 +- .../Account Server Services/EnterpriseServerApi.swift | 4 ++-- .../ClientConfiguration.swift | 2 +- .../Compose Message Service/ComposeMessageService.swift | 2 +- .../Services/Remote Pub Key Services/WkdApi.swift | 3 ++- FlowCryptCommon/Extensions/StringExtensions.swift | 9 ++------- 6 files changed, 9 insertions(+), 13 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 89a08f221..54110c594 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -58,7 +58,7 @@ final class ComposeViewController: TableNodeViewController { private let email: String private var isMessagePasswordSupported: Bool { - guard let domain = email.emailDomain else { return false } + guard let domain = email.emailParts?.domain else { return false } let senderDomainsWithMessagePasswordSupport = ["flowcrypt.com"] return senderDomainsWithMessagePasswordSupport.contains(domain) } diff --git a/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift b/FlowCrypt/Functionality/Services/Account Server Services/EnterpriseServerApi.swift index 055b29477..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.emailDomain, + 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.emailDomain else { + guard let userDomain = email.emailParts?.domain else { throw EnterpriseServerApiError.emailFormat } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift index 020320d83..250ccabd7 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfiguration.swift @@ -128,7 +128,7 @@ class ClientConfiguration { return false } - guard let recipientDomain = recipient.emailDomain else { + 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/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 8f33123a7..b324aaff1 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -117,7 +117,7 @@ final class ComposeMessageService { } private func isMessagePasswordSupported(for email: String) -> Bool { - guard let senderDomain = email.emailDomain else { return false } + guard let senderDomain = email.emailParts?.domain else { return false } let senderDomainsWithMessagePasswordSupport = ["flowcrypt.com"] return senderDomainsWithMessagePasswordSupport.contains(senderDomain) } diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift index b3e62602c..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.emailDomain ?? ""), + 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/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index 64f5b0072..e8d6ebece 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -61,14 +61,9 @@ public extension NSAttributedString { // MARK: Email parsing public extension String { - var email: (username: String?, domain: String?) { + var emailParts: (username: String, domain: String)? { let parts = self.split(separator: "@") - if parts.count != 2 { - return (nil, nil) - } + guard parts.count == 2 else { return nil } return (String(parts[0]), String(parts[1])) } - - var emailUsername: String? { email.username } - var emailDomain: String? { email.domain } } From 43df3d9918411d08b95f90f90115d4a6f16871b5 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 24 Dec 2021 17:18:24 +0200 Subject: [PATCH 14/18] #1221 fix tests --- FlowCrypt/Controllers/Compose/ComposeViewController.swift | 4 ++-- appium/tests/screenobjects/mail-folder.screen.ts | 1 + appium/tests/screenobjects/new-message.screen.ts | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 54110c594..f96e6c544 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -515,7 +515,7 @@ extension ComposeViewController { DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in guard let self = self else { return } - if case MessageValidationError.missedPublicKey = error, self.isMessagePasswordSupported { + if case MessageValidationError.noPubRecipients = error, self.isMessagePasswordSupported { self.setMessagePassword() } else { self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage) @@ -600,7 +600,7 @@ 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() 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 ee9abd16c..ef1ff1a02 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -95,15 +95,17 @@ class NewMessageScreen extends BaseScreen { 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); }; @@ -119,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}`)); }; From 6fd51fbeff7db6b4c621dfd56d2f8a336134090d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 24 Dec 2021 22:27:25 +0200 Subject: [PATCH 15/18] #1221 add messagePassword getter --- .../Compose Message Service/ComposeMessageContext.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 68d162689..d85112f6f 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -12,8 +12,15 @@ struct ComposeMessageContext: Equatable { var message: String? var recipients: [ComposeMessageRecipient] = [] var subject: String? - var messagePassword: String? var attachments: [MessageAttachment] = [] + var messagePassword: String? { + get { + (_messagePassword ?? "").isNotEmpty ? _messagePassword : nil + } + set { _messagePassword = newValue } + } + + private var _messagePassword: String? } extension ComposeMessageContext { From 2bf0acfd3ddbd3a181c5967f3a16d19c379bb3fd Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 24 Dec 2021 23:33:26 +0200 Subject: [PATCH 16/18] #1221 message password alert update --- .../Compose/ComposeViewController.swift | 50 ++++++++++++------- .../ComposeMessageContext.swift | 2 +- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index f96e6c544..a3977332e 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -75,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 @@ -1035,27 +1036,42 @@ extension ComposeViewController { private func enterMessagePassword() async -> String? { return await withCheckedContinuation { (continuation: CheckedContinuation) in - let alert = UIAlertController( - title: "compose_password_modal_title".localized, - message: "compose_password_modal_message".localized, - preferredStyle: .alert - ) + self.messagePasswordAlertController = createMessagePasswordAlert(continuation: continuation) + self.present(self.messagePasswordAlertController!, animated: true, completion: nil) + } + } - alert.addTextField { [weak self] textField in - textField.isSecureTextEntry = true - textField.text = self?.contextToSend.messagePassword - textField.accessibilityLabel = "aid-message-password-textfield" - } + private func createMessagePasswordAlert(continuation: CheckedContinuation) -> UIAlertController { + let alert = UIAlertController( + title: "compose_password_modal_title".localized, + message: "compose_password_modal_message".localized, + preferredStyle: .alert + ) - alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { [weak self] _ in - return continuation.resume(returning: self?.contextToSend.messagePassword) - }) - alert.addAction(UIAlertAction(title: "set".localized, style: .default) { [weak alert] _ in - return continuation.resume(returning: alert?.textFields?[0].text) - }) + 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) - self.present(alert, animated: true, completion: nil) + 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 } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index d85112f6f..906f0436e 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -25,7 +25,7 @@ struct ComposeMessageContext: Equatable { extension ComposeMessageContext { var hasMessagePassword: Bool { - (messagePassword ?? "").isNotEmpty + messagePassword != nil } var hasRecipientsWithoutPubKey: Bool { From 75d353e6122d392a0cfb6aefb42fc48117842400 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Sun, 26 Dec 2021 23:36:48 +0200 Subject: [PATCH 17/18] #1221 fix recipient selection crash --- .../Compose/ComposeViewController.swift | 23 +++++++++++++++---- .../ComposeMessageContext.swift | 10 ++++---- .../ComposeMessageRecipient.swift | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index a3977332e..042d233b8 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -409,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 { @@ -922,7 +926,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 } @@ -930,7 +934,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) } @@ -950,9 +954,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) ) } @@ -969,11 +976,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): @@ -985,6 +995,7 @@ extension ComposeViewController { guard let recipientIndex = index else { return } contextToSend.recipients[recipientIndex].state = state + contextToSend.recipients[recipientIndex].keyState = keyState node.reloadSections([Section.password.rawValue], with: .automatic) node.reloadRows(at: [recipientsIndexPath], with: .automatic) @@ -1018,7 +1029,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) diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 906f0436e..7e22011ec 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -29,9 +29,11 @@ extension ComposeMessageContext { } var hasRecipientsWithoutPubKey: Bool { - recipients.first { - if case .keyNotFound = $0.state { return true } - return false - } != nil + 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 index c0a2e82b9..18094f5f9 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageRecipient.swift @@ -11,6 +11,7 @@ import Foundation struct ComposeMessageRecipient { let email: String var state: RecipientState + var keyState: PubKeyState? } extension ComposeMessageRecipient: Equatable { From 8aeea42d719d369ee2264865e2cfbed73ecca223 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 27 Dec 2021 13:34:18 +0200 Subject: [PATCH 18/18] #1221 add ui test for recipient deletion --- .../Compose/ComposeViewController.swift | 3 ++- .../ComposeMessageContext.swift | 15 +++++++++++++++ FlowCryptUI/Cell Nodes/RecipientEmailNode.swift | 4 ++-- appium/tests/screenobjects/new-message.screen.ts | 6 ++++++ .../SendEmailToRecipientWithoutPublicKey.spec.ts | 8 ++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 042d233b8..e6aa0ba6b 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -887,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 } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift index 7e22011ec..ca8ce8d97 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageContext.swift @@ -23,6 +23,21 @@ struct ComposeMessageContext: Equatable { 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 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/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index ef1ff1a02..64da7d7aa 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -165,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; diff --git a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 3b509d3ba..85cd96577 100644 --- a/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/live/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -33,6 +33,14 @@ describe('COMPOSE EMAIL: ', () => { 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 NewMessageScreen.clickPasswordCell(); await NewMessageScreen.setMessagePassword(emailPassword); await NewMessageScreen.checkPasswordCell(addedPasswordMessage);