diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift index 9320430c4..c940beb01 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+ErrorHandling.swift @@ -66,17 +66,11 @@ extension ComposeViewController { } switch error { - case MessageValidationError.noPubRecipients: - guard isMessagePasswordSupported else { - showPlainMessageConfirmationAlert() - return - } - - if Bundle.isEnterprise { - setMessagePassword() - } else { - showPlainMessageAlert() - } + case MessageValidationError.noPubRecipients, + MessageValidationError.revokedKeyRecipients, + MessageValidationError.expiredKeyRecipients, + MessageValidationError.notUsableForEncryptionKeyRecipients: + processSendMessageWithNoValidKeys(error: error) case MessageValidationError.notUniquePassword, MessageValidationError.subjectContainsPassword, MessageValidationError.weakPassword: @@ -86,26 +80,41 @@ extension ComposeViewController { } } - private func showPlainMessageConfirmationAlert() { - showAlertWithAction( + private func processSendMessageWithNoValidKeys(error: Error) { + let alert = UIAlertController( title: "compose_message_encryption".localized, - message: "compose_plain_message_confirmation".localized, - actionButtonTitle: "compose_send_unencrypted".localized, - actionAccessibilityIdentifier: "aid-compose-send-plain", - onAction: { [weak self] _ in self?.handleSendTap(shouldSendPlainMessage: true) } + message: error.errorMessage, + preferredStyle: .alert ) - } - - private func showPlainMessageAlert() { - showAlertWithAction( - title: "compose_message_encryption".localized, - message: "compose_plain_message_alert".localized, - cancelButtonTitle: "compose_add_message_password".localized, - actionButtonTitle: "compose_send_unencrypted".localized, - actionAccessibilityIdentifier: "aid-compose-send-plain", - onAction: { [weak self] _ in self?.handleSendTap(shouldSendPlainMessage: true) }, - onCancel: { [weak self] _ in self?.setMessagePassword() } + let sendUnEncryptedAction = UIAlertAction( + title: "compose_send_unencrypted".localized, + style: .default, + handler: { [weak self] _ in + self?.handleSendTap(shouldSendPlainMessage: true) + } + ) + sendUnEncryptedAction.accessibilityIdentifier = "aid-compose-send-plain" + let sendPasswordProtectedAction = UIAlertAction( + title: "compose_add_message_password".localized, + style: .default, + handler: { [weak self] _ in + self?.setMessagePassword() + } ) + sendPasswordProtectedAction.accessibilityIdentifier = "aid-compose-send-message-password" + let cancelAction = UIAlertAction( + title: "cancel".localized, + style: .cancel + ) + cancelAction.accessibilityIdentifier = "aid-cancel-button" + if !Bundle.isEnterprise { + alert.addAction(sendUnEncryptedAction) // Disallow sending plain message for enterprise + } + if isMessagePasswordSupported { + alert.addAction(sendPasswordProtectedAction) + } + alert.addAction(cancelAction) + present(alert, animated: true, completion: nil) } private func reEnableSendButton() { diff --git a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift index 55f82b34b..c8832a994 100644 --- a/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift +++ b/FlowCrypt/Controllers/Compose/Extensions/ComposeViewController+TableView.swift @@ -29,7 +29,7 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.main, .recipients(.cc)), (.main, .recipients(.bcc)): return !shouldShowEmailRecipientsLabel && shouldShowAllRecipientTypes ? 1 : 0 case (.main, .password): - return isMessagePasswordSupported && contextToSend.hasRecipientsWithoutPubKey ? 1 : 0 + return isMessagePasswordSupported && contextToSend.hasRecipientWithInvalidKey ? 1 : 0 case (.main, .compose): return ComposePart.allCases.count case (.main, .attachments): diff --git a/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageContext.swift b/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageContext.swift index cb82913c4..c7f56001b 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageContext.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageContext.swift @@ -55,6 +55,10 @@ extension ComposeMessageContext { recipients.contains(where: { $0.keyState == .active }) } + var hasRecipientWithInvalidKey: Bool { + recipients.contains(where: { $0.keyState != .active }) + } + var hasRecipientsWithoutPubKey: Bool { recipients.contains(where: { $0.keyState == .empty }) } diff --git a/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageHelper.swift b/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageHelper.swift index 74dff45d2..16171540f 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageHelper.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Helper/ComposeMessageHelper.swift @@ -232,13 +232,13 @@ final class ComposeMessageHelper { throw MessageValidationError.noPubRecipients } - guard !contains(keyState: .notUsableForEncryption) else { + guard hasMessagePassword || !contains(keyState: .notUsableForEncryption) else { throw MessageValidationError.notUsableForEncryptionKeyRecipients } - guard !contains(keyState: .expired) else { + guard hasMessagePassword || !contains(keyState: .expired) else { throw MessageValidationError.expiredKeyRecipients } - guard !contains(keyState: .revoked) else { + guard hasMessagePassword || !contains(keyState: .revoked) else { throw MessageValidationError.revokedKeyRecipients } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 4be5a3b32..44b652fa0 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -137,7 +137,7 @@ "compose_sign_passphrase_required" = "Passphrase is required for message signing."; "compose_sign_passphrase_no_match" = "This pass phrase did not match your signing private key."; "compose_sign_no_keys" = "Cannot sign message: none of your %@ account keys can be used for sending address %@"; -"compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up."; +"compose_password_placeholder" = "Tap to add password for recipients without encryption or those with an invalid public key."; "compose_password_set_message" = "Web portal password added"; "compose_password_modal_title" = "Set web portal password"; "compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)\n\nPassword should include:\n- one uppercase\n- one lowercase\n- one number\n- one special character eg &/#\"-'_%-@,;:!*()\n- min 8 characters length"; diff --git a/appium/tests/data/index.ts b/appium/tests/data/index.ts index 1ce7a563b..2cded8cd9 100644 --- a/appium/tests/data/index.ts +++ b/appium/tests/data/index.ts @@ -204,8 +204,8 @@ export const CommonData = { password: 'abcABC1*', 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)\n\nPassword should include: - one uppercase - one lowercase - one number - one special character eg &/#"-'_%-@,;:!*() - min 8 characters length`, plainMessageModal: - "Message Encryption\n One or more of your recipients don't have encryption set up.\n\nPlease add a message password, or message will be sent unencrypted.", - emptyPasswordMessage: "Tap to add password for recipients who don't have encryption set up.", + 'Message Encryption\nOne or more of your recipients are missing a public key (marked in gray).\n\nPlease ask them to share it with you, or ask them to also set up FlowCrypt.', + emptyPasswordMessage: 'Tap to add password for recipients without encryption or those with an invalid public key.', addedPasswordMessage: 'Web portal password added', weakPasswordMessage: "Error\nPassword didn't comply with company policy, which requires at least:\n\n- one uppercase - one lowercase - one number - one special character eg &/#\"-'_%-@,;:!*() - 8 characters length\n\nPlease update the password and re-send.", @@ -220,36 +220,11 @@ export const CommonData = { subject: 'Honor reply-to address - plain', }, errors: { - noPublicKey: - 'Error\n' + - '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.', wrongPassPhrase: 'Error\n' + 'Could not compose message\n' + '\n' + 'This pass phrase did not match your signing private key.', - notUsableEncryptionPublicKey: - 'Error\n' + - 'Could not compose message\n' + - '\n' + - 'One or more of your recipients have sign-only public keys (marked in yellow).\n' + - '\n' + - 'Please ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.', - expiredPublicKey: - 'Error\n' + - '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.', - revokedPublicKey: - 'Error\n' + - '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.', + notUsableEncryptionPublicKey: `Message Encryption\nOne or more of your recipients have sign-only public keys (marked in yellow).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.`, + expiredPublicKey: `Message Encryption\nOne 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.`, + revokedPublicKey: `Message Encryption\nOne 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.`, wrongPassPhraseOnLogin: 'Error\n' + 'Wrong pass phrase, please try again', attachmentDecryptKeyMismatchError: 'Error decrypting attachment\n' + diff --git a/appium/tests/helpers/ElementHelper.ts b/appium/tests/helpers/ElementHelper.ts index a772bba87..13c272e76 100644 --- a/appium/tests/helpers/ElementHelper.ts +++ b/appium/tests/helpers/ElementHelper.ts @@ -86,9 +86,13 @@ class ElementHelper { } await this.waitAndClick(element); - await this.waitAndClick(element); - + await browser.pause(200); const selectAllButton = await $('~Select All'); + // Check if 'Select All' is not displayed, if so, click the element again. + if (!(await selectAllButton.isDisplayed())) { + await this.waitAndClick(element); + } + await this.waitAndClick(selectAllButton); await driver.sendKeys(['\b']); // backspace diff --git a/appium/tests/helpers/TouchHelper.ts b/appium/tests/helpers/TouchHelper.ts index 03d3f4107..962c6f2ba 100644 --- a/appium/tests/helpers/TouchHelper.ts +++ b/appium/tests/helpers/TouchHelper.ts @@ -59,8 +59,12 @@ class TouchHelper { pressOptions = { x: width, y: height }; break; } + await TouchHelper.tapScreenAt(pressOptions); + }; + + static tapScreenAt = async ({ x, y }: { x: number; y: number }) => { await driver.touchPerform([ - { action: 'press', options: pressOptions }, + { action: 'press', options: { x, y } }, { action: 'wait', options: { ms: 100 } }, { action: 'release', options: {} }, ]); diff --git a/appium/tests/screenobjects/attachment.screen.ts b/appium/tests/screenobjects/attachment.screen.ts index 21d840a77..e0bffcf68 100644 --- a/appium/tests/screenobjects/attachment.screen.ts +++ b/appium/tests/screenobjects/attachment.screen.ts @@ -3,7 +3,6 @@ import ElementHelper from '../helpers/ElementHelper'; const SELECTORS = { BACK_BTN: '~aid-back-button', - SYSTEM_BACK_BTN: '~Back', SAVE_BTN: '~aid-save-attachment-to-device', CANCEL_BTN: '~Cancel', // can't change aid for UIDocumentPickerViewController }; @@ -17,10 +16,6 @@ class AttachmentScreen extends BaseScreen { return $(SELECTORS.BACK_BTN); } - get systemBackButton() { - return $(SELECTORS.SYSTEM_BACK_BTN); - } - get saveButton() { return $(SELECTORS.SAVE_BTN); } @@ -42,7 +37,17 @@ class AttachmentScreen extends BaseScreen { clickSystemBackButton = async () => { // Due to a issue in SemaphoreCI environment, a single back button click does not yield the expected behavior. // Therefore, we have implemented a mechanism to continuously click the system back button until cancel button appears. - await ElementHelper.clickUntilExpectedElementAppears(await this.systemBackButton, await this.cancelButton, 10); + await browser.pause(1000); + let systemBackButton = await $('~Back'); + if (!(await systemBackButton.isDisplayed())) { + const browseButton = await $('~Browse'); // Back button is renamed to Browse in newer iOS versions + if (await browseButton.isDisplayed()) { + systemBackButton = browseButton; + } else { + throw new Error('System backup button is not displayed either using Back or Browse selector'); + } + } + await ElementHelper.clickUntilExpectedElementAppears(systemBackButton, await this.cancelButton, 10); }; clickCancelButton = async () => { diff --git a/appium/tests/screenobjects/new-message.screen.ts b/appium/tests/screenobjects/new-message.screen.ts index 1663ff893..99a3a6ab0 100644 --- a/appium/tests/screenobjects/new-message.screen.ts +++ b/appium/tests/screenobjects/new-message.screen.ts @@ -177,6 +177,10 @@ class NewMessageScreen extends BaseScreen { setComposeSecurityMessage = async (message: string) => { await browser.pause(500); const el = await this.composeSecurityMessage; + // click message field so that message field is focused + // https://github.com/FlowCrypt/flowcrypt-ios/pull/2429#discussion_r1389511041 + const location = await el.getLocation(); + await TouchHelper.tapScreenAt({ x: location.x + 5, y: location.y + 5 }); await ElementHelper.clearInput(el); await ElementHelper.waitClickAndType(el, message); }; @@ -391,6 +395,10 @@ class NewMessageScreen extends BaseScreen { await ElementHelper.waitAndClick(await this.sendButton); }; + checkSendPlainMessageButtonNotPresent = async () => { + await ElementHelper.waitElementInvisible(await this.sendPlainMessageButton); + }; + clickSendPlainMessageButton = async () => { await ElementHelper.waitAndClick(await this.sendPlainMessageButton); }; diff --git a/appium/tests/specs/mock/composeEmail/CheckSignOnlyKey.spec.ts b/appium/tests/specs/mock/composeEmail/CheckSignOnlyKey.spec.ts index 0ed4b309d..82fd332af 100644 --- a/appium/tests/specs/mock/composeEmail/CheckSignOnlyKey.spec.ts +++ b/appium/tests/specs/mock/composeEmail/CheckSignOnlyKey.spec.ts @@ -39,7 +39,7 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.composeEmail(recipient.email, subject, message); await NewMessageScreen.clickSendButton(); await BaseScreen.checkModalMessage(notUsableEncryptionPublicKeyError); - await BaseScreen.clickOkButtonOnError(); + await BaseScreen.clickCancelButton(); // Stage2: Now try to encrypt & send message for user which contains sign only key & normal key and check if message is sent correctly mockApi.attesterConfig = { diff --git a/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithExpiredAndRevokedPublicKeys.spec.ts b/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithExpiredAndRevokedPublicKeys.spec.ts index 7b1996d7d..03476399b 100644 --- a/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithExpiredAndRevokedPublicKeys.spec.ts +++ b/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithExpiredAndRevokedPublicKeys.spec.ts @@ -46,7 +46,7 @@ describe('COMPOSE EMAIL: ', () => { await BaseScreen.checkModalMessage(expiredPublicKeyError); - await BaseScreen.clickOkButtonOnError(); + await BaseScreen.clickCancelButton(); await NewMessageScreen.clickBackButton(); await MailFolderScreen.checkInboxScreen(); diff --git a/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts b/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts index 743b701d8..cb75c576e 100644 --- a/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts +++ b/appium/tests/specs/mock/composeEmail/SendEmailToRecipientWithoutPublicKey.spec.ts @@ -24,7 +24,6 @@ describe('COMPOSE EMAIL: ', () => { const emailPassword = CommonData.recipientWithoutPublicKey.password; const plainMessageModal = CommonData.recipientWithoutPublicKey.plainMessageModal; - const passwordModalMessage = CommonData.recipientWithoutPublicKey.modalMessage; const emptyPasswordMessage = CommonData.recipientWithoutPublicKey.emptyPasswordMessage; const subjectPasswordErrorMessage = CommonData.recipientWithoutPublicKey.subjectPasswordErrorMessage; const addedPasswordMessage = CommonData.recipientWithoutPublicKey.addedPasswordMessage; @@ -58,8 +57,6 @@ describe('COMPOSE EMAIL: ', () => { await BaseScreen.checkModalMessage(plainMessageModal); await NewMessageScreen.clickCancelButton(); - await BaseScreen.checkModalMessage(passwordModalMessage); - await NewMessageScreen.clickCancelButton(); await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); await NewMessageScreen.deleteAddedRecipient(0); @@ -68,8 +65,6 @@ describe('COMPOSE EMAIL: ', () => { await NewMessageScreen.clickSendButton(); await BaseScreen.checkModalMessage(plainMessageModal); await NewMessageScreen.clickCancelButton(); - await BaseScreen.checkModalMessage(passwordModalMessage); - await NewMessageScreen.clickCancelButton(); await NewMessageScreen.checkPasswordCell(emptyPasswordMessage); // check message password validation @@ -112,7 +107,7 @@ describe('COMPOSE EMAIL: ', () => { message: emailText, }); await NewMessageScreen.clickSendButton(); - await BaseScreen.checkModalMessage(passwordModalMessage); + await NewMessageScreen.checkSendPlainMessageButtonNotPresent(); }); }); });