From 332ed777c8104decbc8a2fdfe766c62b6a3295c9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 16 Nov 2021 13:34:21 +0200 Subject: [PATCH 01/15] issue #530 ask only for mail permission on sign up --- .../CheckAuthScopesViewController.swift | 15 +----- .../Services/GeneralConstants.swift | 6 ++- .../Services/GoogleUserService.swift | 48 +++++++++---------- .../Resources/en.lproj/Localizable.strings | 2 +- 4 files changed, 28 insertions(+), 43 deletions(-) diff --git a/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift b/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift index 122894dcf..60a61ea62 100644 --- a/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift +++ b/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift @@ -10,19 +10,7 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI -private extension GoogleScope { - var title: String { - switch self { - case .userInfo: return "User Info" - case .mail: return "Gmail" - case .contacts: return "Contacts" - case .otherContacts: return "Other Contacts" - } - } -} - class CheckAuthScopesViewController: TableNodeViewController { - private let missingScopes: [GoogleScope] private let globalRouter: GlobalRouterType @@ -46,8 +34,7 @@ class CheckAuthScopesViewController: TableNodeViewController { } var errorMessage: String { - let scopesMessage = missingScopes.map { $0.title }.joined(separator: ", ") - return "gmail_service_no_access_to_account_message".localizeWithArguments(scopesMessage) + "gmail_service_no_access_to_account_message".localized } } diff --git a/FlowCrypt/Functionality/Services/GeneralConstants.swift b/FlowCrypt/Functionality/Services/GeneralConstants.swift index 8b929b8cc..66c6ac6cf 100644 --- a/FlowCrypt/Functionality/Services/GeneralConstants.swift +++ b/FlowCrypt/Functionality/Services/GeneralConstants.swift @@ -8,7 +8,8 @@ enum GeneralConstants { enum Gmail { static let clientID = "679326713487-5r16ir2f57bpmuh2d6dal1bcm9m1ffqc.apps.googleusercontent.com" static let redirectURL = URL(string: "com.googleusercontent.apps.679326713487-5r16ir2f57bpmuh2d6dal1bcm9m1ffqc:/oauthredirect")! - static let currentScope: [GoogleScope] = GoogleScope.allCases + static let basicScope: [GoogleScope] = [.userInfo, .userEmail, .mail] + static let contactsScope: [GoogleScope] = [.contacts, .otherContacts] } enum Global { @@ -28,11 +29,12 @@ enum GeneralConstants { } enum GoogleScope: CaseIterable { - case userInfo, mail, contacts, otherContacts + case userInfo, userEmail, mail, contacts, otherContacts var value: String { switch self { case .userInfo: return "https://www.googleapis.com/auth/userinfo.profile" + case .userEmail: return "https://www.googleapis.com/auth/userinfo.email" case .mail: return "https://mail.google.com/" case .contacts: return "https://www.googleapis.com/auth/contacts" case .otherContacts: return "https://www.googleapis.com/auth/contacts.other.readonly" diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index fc97f1009..005cb5a60 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -73,30 +73,27 @@ extension GoogleUserService: UserServiceType { // GTMAppAuth should renew session via OIDAuthStateChangeDelegate } - func signIn(in viewController: UIViewController) async throws -> SessionType { + @MainActor func signIn(in viewController: UIViewController) async throws -> SessionType { return try await withCheckedThrowingContinuation { continuation in - DispatchQueue.main.async { - // todo - should be fixed with MainActor instead? - // Google doesn't like to be called on non-main thread - let request = self.makeAuthorizationRequest() - let googleAuthSession = OIDAuthState.authState( - byPresenting: request, - presenting: viewController - ) { authState, error in - if let authState = authState { - Task { - do { - return continuation.resume(returning: try await self.handleGoogleAuthStateResult(authState)) - } catch { - return continuation.resume(throwing: error) - } - } - return + let request = self.makeAuthorizationRequest() + let googleAuthSession = OIDAuthState.authState( + byPresenting: request, + presenting: viewController + ) { authState, authError in + guard let authState = authState else { + let error = authError ?? AppErr.unexpected("Shouldn't happen because covered received non nil error and non nil authState") + return continuation.resume(throwing: error) + } + + Task { + do { + return continuation.resume(returning: try await self.handleGoogleAuthStateResult(authState)) + } catch { + return continuation.resume(throwing: error) } - return continuation.resume(throwing: error ?? AppErr.unexpected("Shouldn't happen because covered received non nil error and non nil authState")) } - self.appDelegate?.googleAuthSession = googleAuthSession } + self.appDelegate?.googleAuthSession = googleAuthSession } } @@ -109,7 +106,7 @@ extension GoogleUserService: UserServiceType { private func handleGoogleAuthStateResult(_ authState: OIDAuthState) async throws -> SessionType { let missingScopes = self.checkMissingScopes(authState.scope) - if !missingScopes.isEmpty { + if missingScopes.isNotEmpty { throw GoogleUserServiceError.userNotAllowedAllNeededScopes(missingScopes: missingScopes) } let authorization = GTMAppAuthFetcherAuthorization(authState: authState) @@ -132,7 +129,7 @@ extension GoogleUserService { OIDAuthorizationRequest( configuration: GTMAppAuthFetcherAuthorization.configurationForGoogle(), clientId: GeneralConstants.Gmail.clientID, - scopes: GeneralConstants.Gmail.currentScope.map(\.value) + [OIDScopeEmail], + scopes: GeneralConstants.Gmail.basicScope.map(\.value), redirectURL: GeneralConstants.Gmail.redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil @@ -180,10 +177,9 @@ extension GoogleUserService { } private func checkMissingScopes(_ scope: String?) -> [GoogleScope] { - guard let scope = scope else { - return GoogleScope.allCases - } - return GoogleScope.allCases.filter { !scope.contains($0.value) } + let authScope = GeneralConstants.Gmail.basicScope + guard let scope = scope else { return authScope } + return authScope.filter { !scope.contains($0.value) } } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 41d2cd18e..0061ac97a 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -257,7 +257,7 @@ "gmail_service_missing_message_info_error_message" = "Missing message info: %@"; "gmail_service_provider_error_error_message" = "Gmail provider error: %@"; "gmail_service_missing_back_query_error_message" = "Missed backup query: %@"; -"gmail_service_no_access_to_account_message" = "Google login successful.\n Next, connect your inbox. Be sure to tick %@ permission on the next screen. The grant is stored locally on your device, never shared with anyone."; +"gmail_service_no_access_to_account_message" = "Google login successful.\n Now connect your inbox on the next screen. The grant is stored locally on your device, never shared with anyone."; // Files picking "files_picking_select_input_source_title" = "Please select input source"; From bec5f0ccc657dc1fdc7bce34de918ea9f7059518 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 17 Nov 2021 16:34:09 +0200 Subject: [PATCH 02/15] issue #530 ask for contacts scope on compose screen --- FlowCrypt.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 8 +-- .../CheckAuthScopesViewController.swift | 15 ++--- .../Compose/ComposeViewController.swift | 63 ++++++++++--------- .../Extensions/UIViewController+Spinner.swift | 2 +- .../CloudContactsProvider.swift | 13 +++- .../Functionality/Services/GlobalRouter.swift | 27 +++++++- .../Services/GoogleUserService.swift | 12 ++-- .../Resources/en.lproj/Localizable.strings | 4 ++ FlowCryptUI/Nodes/TextFieldNode.swift | 12 ++-- 10 files changed, 97 insertions(+), 65 deletions(-) rename FlowCrypt/Controllers/{CheckAuthScopes => CheckMailAuth}/CheckAuthScopesViewController.swift (87%) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 9fff98705..f89e657ff 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -1091,12 +1091,12 @@ path = "Key Details"; sourceTree = ""; }; - 601EEE32272B1A5800FE445B /* CheckAuthScopes */ = { + 601EEE32272B1A5800FE445B /* CheckMailAuth */ = { isa = PBXGroup; children = ( 601EEE30272B19D200FE445B /* CheckAuthScopesViewController.swift */, ); - path = CheckAuthScopes; + path = CheckMailAuth; sourceTree = ""; }; 9F0C3C1F23191F2000299985 /* Services */ = { @@ -1680,7 +1680,7 @@ C132B9C51EC2DCAB00763715 /* Controllers */ = { isa = PBXGroup; children = ( - 601EEE32272B1A5800FE445B /* CheckAuthScopes */, + 601EEE32272B1A5800FE445B /* CheckMailAuth */, 04B472911ECE29F600B8266F /* SideMenu */, D2FF6969243115FE007182F0 /* SetupImap */, 32DCA8D5AF0A43354CC7F58B /* SignIn */, diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0fb96c3e3..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": "328425bfc372ce77ec1f4f2701f61ececbb97d84", - "version": "10.19.0" + "revision": "bdbbd57f411a0f4e72b359113dbc6d23fdf96680", + "version": "10.20.0" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/realm/realm-core", "state": { "branch": null, - "revision": "b170db6a47789ff5f2fbc3eeed0220b4b0a3f6b7", - "version": "11.6.0" + "revision": "c3c11a841642ac93c27bd1edd61f989fc0bfb809", + "version": "11.6.1" } }, { diff --git a/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift similarity index 87% rename from FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift rename to FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift index 60a61ea62..b1a5d32c7 100644 --- a/FlowCrypt/Controllers/CheckAuthScopes/CheckAuthScopesViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift @@ -1,5 +1,5 @@ // -// CheckAuthScopesViewController.swift +// CheckMailAuthViewController.swift // FlowCrypt // // Created by Yevhen Kyivskyi on 28.10.2021 @@ -10,15 +10,10 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI -class CheckAuthScopesViewController: TableNodeViewController { - private let missingScopes: [GoogleScope] +class CheckMailAuthViewController: TableNodeViewController { private let globalRouter: GlobalRouterType - init( - missingScopes: [GoogleScope], - globalRouter: GlobalRouterType = GlobalRouter() - ) { - self.missingScopes = missingScopes + init(globalRouter: GlobalRouterType = GlobalRouter()) { self.globalRouter = globalRouter super.init(node: TableNode()) } @@ -39,7 +34,7 @@ class CheckAuthScopesViewController: TableNodeViewController { } // MARK: - ASTableDelegate, ASTableDataSource -extension CheckAuthScopesViewController: ASTableDelegate, ASTableDataSource { +extension CheckMailAuthViewController: ASTableDelegate, ASTableDataSource { func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { return 3 } @@ -53,7 +48,7 @@ extension CheckAuthScopesViewController: ASTableDelegate, ASTableDataSource { } // MARK: - UI -extension CheckAuthScopesViewController { +extension CheckMailAuthViewController { private func setupUI() { node.delegate = self node.dataSource = self diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 339644693..6dfc0e3c6 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -27,7 +27,7 @@ final class ComposeViewController: TableNodeViewController { private enum Constants { static let endTypingCharacters = [",", " ", "\n", ";"] - static let shouldShowScopeAlertIndex = "indexShould_ShowScope" + static let didShowContactsScopeAlert = "didShowContactsScopeAlert" } enum State { @@ -46,12 +46,14 @@ final class ComposeViewController: TableNodeViewController { private let notificationCenter: NotificationCenter private let decorator: ComposeViewDecorator private let contactsService: ContactsServiceType + private let cloudContactProvider: CloudContactsProvider private let filesManager: FilesManagerType private let photosManager: PhotosManagerType private let keyService: KeyServiceType private let keyMethods: KeyMethodsType private let service: ServiceActor private let passPhraseService: PassPhraseService + private let router: GlobalRouterType private let search = PassthroughSubject() private let userDefaults: UserDefaults @@ -80,7 +82,8 @@ final class ComposeViewController: TableNodeViewController { photosManager: PhotosManagerType = PhotosManager(), keyService: KeyServiceType = KeyService(), passPhraseService: PassPhraseService = PassPhraseService(), - keyMethods: KeyMethodsType = KeyMethods() + keyMethods: KeyMethodsType = KeyMethods(), + router: GlobalRouterType = GlobalRouter() ) { self.email = email self.notificationCenter = notificationCenter @@ -88,6 +91,7 @@ final class ComposeViewController: TableNodeViewController { self.decorator = decorator self.userDefaults = userDefaults self.contactsService = contactsService + self.cloudContactProvider = cloudContactProvider self.composeMessageService = composeMessageService self.filesManager = filesManager self.photosManager = photosManager @@ -99,6 +103,7 @@ final class ComposeViewController: TableNodeViewController { cloudContactProvider: cloudContactProvider ) self.passPhraseService = passPhraseService + self.router = router self.contextToSend.subject = input.subject super.init(node: TableNode()) } @@ -110,7 +115,7 @@ final class ComposeViewController: TableNodeViewController { override func viewDidLoad() { super.viewDidLoad() - + userDefaults.removeObject(forKey: Constants.didShowContactsScopeAlert) setupUI() setupNavigationBar() observeKeyboardNotifications() @@ -126,7 +131,6 @@ final class ComposeViewController: TableNodeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - showScopeAlertIfNeeded() cancellable.forEach { $0.cancel() } setupSearch() startDraftTimer() @@ -743,6 +747,7 @@ extension ComposeViewController { } private func handleDidBeginEditing() { + showNoAccessToContactsAlertIfNeeded() node.view.keyboardDismissMode = .none } } @@ -932,7 +937,6 @@ extension ComposeViewController: PHPickerViewControllerDelegate { }) } } - } } @@ -1066,35 +1070,34 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } -} -extension ComposeViewController { - private func showScopeAlertIfNeeded() { - if shouldRenewToken(for: [.mail]), - !userDefaults.bool(forKey: Constants.shouldShowScopeAlertIndex) { - userDefaults.set(true, forKey: Constants.shouldShowScopeAlertIndex) - let alert = UIAlertController( - title: "", - message: "compose_enable_search".localized, - preferredStyle: .alert - ) - let okAction = UIAlertAction( - title: "Log out", - style: .default - ) { _ in } - let cancelAction = UIAlertAction( - title: "cancel".localized, - style: .destructive - ) { _ in } - alert.addAction(okAction) - alert.addAction(cancelAction) + private func showNoAccessToContactsAlertIfNeeded() { + guard !cloudContactProvider.isContactsScopeEnabled, + !userDefaults.bool(forKey: Constants.didShowContactsScopeAlert) + else { return } - present(alert, animated: true, completion: nil) + userDefaults.set(true, forKey: Constants.didShowContactsScopeAlert) + + let alert = UIAlertController( + title: "compose_contacts_search".localized, + message: "compose_enable_contacts_search".localized, + preferredStyle: .alert + ) + let laterAction = UIAlertAction( + title: "later".localized, + style: .cancel + ) + let allowAction = UIAlertAction( + title: "allow".localized, + style: .default + ) { [weak self] _ in + guard let self = self else { return } + self.router.askForContactsPermission(for: .gmailLogin(self)) } - } + alert.addAction(allowAction) + alert.addAction(laterAction) - private func shouldRenewToken(for newScope: [GoogleScope]) -> Bool { - false + present(alert, animated: true, completion: nil) } } diff --git a/FlowCrypt/Extensions/UIViewController+Spinner.swift b/FlowCrypt/Extensions/UIViewController+Spinner.swift index f8d3cdf8a..b15ef376a 100644 --- a/FlowCrypt/Extensions/UIViewController+Spinner.swift +++ b/FlowCrypt/Extensions/UIViewController+Spinner.swift @@ -5,7 +5,7 @@ // Created by Roma Sosnovsky on 25/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import MBProgressHUD import UIKit diff --git a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift index 1f594fdbd..ba1a9332f 100644 --- a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift @@ -10,6 +10,7 @@ import FlowCryptCommon import GoogleAPIClientForREST_PeopleService protocol CloudContactsProvider { + var isContactsScopeEnabled: Bool { get } func searchContacts(query: String) async throws -> [String] } @@ -22,7 +23,7 @@ enum CloudContactsProviderError: Error { final class UserContactsProvider { private let logger = Logger.nested("UserContactsProvider") - private let userService: GoogleUserServiceType + private let userService: GoogleUserServiceType & UserServiceType private var peopleService: GTLRPeopleServiceService { let service = GTLRPeopleServiceService() @@ -59,7 +60,14 @@ final class UserContactsProvider { } } - init(userService: GoogleUserServiceType = GoogleUserService()) { + var isContactsScopeEnabled: Bool { + guard let currentScopeString = userService.authorization?.authState.scope else { return false } + let currentScope = currentScopeString.split(separator: ",").map(String.init) + let contactsScope = GeneralConstants.Gmail.contactsScope.map(\.value) + return contactsScope.allSatisfy(currentScope.contains) + } + + init(userService: GoogleUserServiceType & UserServiceType = GoogleUserService()) { self.userService = userService runWarmupQuery() @@ -75,6 +83,7 @@ final class UserContactsProvider { extension UserContactsProvider: CloudContactsProvider { func searchContacts(query: String) async -> [String] { + guard isContactsScopeEnabled else { return [] } let contacts = await searchUserContacts(query: query, type: .contacts) let otherContacts = await searchUserContacts(query: query, type: .other) let emails = Set(contacts + otherContacts) diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 11657e79e..f95e01af5 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -13,6 +13,7 @@ protocol GlobalRouterType { @MainActor func proceed() @MainActor func signIn(with route: GlobalRoutingType) @MainActor func switchActive(user: User) + @MainActor func askForContactsPermission(for route: GlobalRoutingType) @MainActor func signOut() } @@ -89,10 +90,10 @@ extension GlobalRouter { @MainActor private func handleGmailError(_ error: Error) { logger.logInfo("gmail login failed with error \(error.localizedDescription)") if let gmailUserError = error as? GoogleUserServiceError, - case .userNotAllowedAllNeededScopes(let missingScopes) = gmailUserError { + case .userNotAllowedAllNeededScopes = gmailUserError { DispatchQueue.main.async { let topNavigation = (self.keyWindow.rootViewController as? UINavigationController) - let checkAuthViewControlelr = CheckAuthScopesViewController(missingScopes: missingScopes) + let checkAuthViewControlelr = CheckMailAuthViewController() topNavigation?.pushViewController(checkAuthViewControlelr, animated: true) } } @@ -108,7 +109,8 @@ extension GlobalRouter { case .gmailLogin(let viewController): Task { do { - let session = try await googleService.signIn(in: viewController) + let scopes = GeneralConstants.Gmail.basicScope.map(\.value) + let session = try await googleService.signIn(in: viewController, scopes: scopes) self.userAccountService.startSessionFor(user: session) self.proceed(with: session) } catch { @@ -132,6 +134,25 @@ extension GlobalRouter { } } + @MainActor func askForContactsPermission(for route: GlobalRoutingType) { + logger.logInfo("Ask for contacts permission with \(route)") + + switch route { + case .gmailLogin(let viewController): + Task { + do { + let scopes = GeneralConstants.Gmail.contactsScope.map(\.value) + let session = try await googleService.signIn(in: viewController, scopes: scopes) + self.userAccountService.startSessionFor(user: session) + } catch { + logger.logInfo("Contacts scope failed with error \(error.errorMessage)") + } + } + case .other: + break + } + } + @MainActor func switchActive(user: User) { logger.logInfo("Switching active user \(user)") guard let session = userAccountService.switchActiveSessionFor(user: user) else { diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index 005cb5a60..d5e9ce689 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -14,7 +14,7 @@ import RealmSwift protocol UserServiceType { func signOut(user email: String) - func signIn(in viewController: UIViewController) async throws -> SessionType + func signIn(in viewController: UIViewController, scopes: [String]) async throws -> SessionType func renewSession() async throws } @@ -73,9 +73,9 @@ extension GoogleUserService: UserServiceType { // GTMAppAuth should renew session via OIDAuthStateChangeDelegate } - @MainActor func signIn(in viewController: UIViewController) async throws -> SessionType { + @MainActor func signIn(in viewController: UIViewController, scopes: [String]) async throws -> SessionType { return try await withCheckedThrowingContinuation { continuation in - let request = self.makeAuthorizationRequest() + let request = self.makeAuthorizationRequest(scopes: scopes) let googleAuthSession = OIDAuthState.authState( byPresenting: request, presenting: viewController @@ -125,11 +125,11 @@ extension GoogleUserService: UserServiceType { // MARK: - Convenience extension GoogleUserService { - private func makeAuthorizationRequest() -> OIDAuthorizationRequest { + private func makeAuthorizationRequest(scopes: [String]) -> OIDAuthorizationRequest { OIDAuthorizationRequest( configuration: GTMAppAuthFetcherAuthorization.configurationForGoogle(), clientId: GeneralConstants.Gmail.clientID, - scopes: GeneralConstants.Gmail.basicScope.map(\.value), + scopes: scopes, redirectURL: GeneralConstants.Gmail.redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil @@ -139,7 +139,7 @@ extension GoogleUserService { // save auth session to keychain private func saveAuth(state: OIDAuthState, for email: String) { state.stateChangeDelegate = self - let authorization: GTMAppAuthFetcherAuthorization = GTMAppAuthFetcherAuthorization(authState: state) + let authorization = GTMAppAuthFetcherAuthorization(authState: state) GTMAppAuthFetcherAuthorization.save(authorization, toKeychainForName: Constants.index + email) } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 0061ac97a..a7a34869a 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -12,6 +12,8 @@ "settings" = "Settings"; "go_back" = "Go back"; "continue" = "Continue"; +"allow" = "Allow"; +"later" = "Later"; // EMAIL "email_removed" = "Email moved to Trash"; @@ -83,6 +85,8 @@ "compose_recipient" = "Add Recipient"; "compose_subject" = "Subject"; "compose_enable_search" = "To enable searching contacts, please Log out and set up FlowCrypt again"; +"compose_enable_contacts_search" = "To enable contacts search, please allow access to your Google contacts on the next screen"; +"compose_contacts_search" = "Contacts Search"; "compose_uploading" = "Uploading"; "compose_sent" = "Sent"; diff --git a/FlowCryptUI/Nodes/TextFieldNode.swift b/FlowCryptUI/Nodes/TextFieldNode.swift index b83ac136b..c144c58b4 100644 --- a/FlowCryptUI/Nodes/TextFieldNode.swift +++ b/FlowCryptUI/Nodes/TextFieldNode.swift @@ -118,14 +118,14 @@ public final class TextFieldNode: ASDisplayNode { private lazy var node = ASDisplayNode { TextField() } - private var textFiledAction: TextFieldAction? + private var textFieldAction: TextFieldAction? private var onToolbarDoneAction: (() -> Void)? public init(preferredHeight: CGFloat?, action: TextFieldAction? = nil, accessibilityIdentifier: String?) { super.init() addSubnode(node) - textFiledAction = action + textFieldAction = action setupTextField(with: accessibilityIdentifier) } @@ -139,7 +139,7 @@ public final class TextFieldNode: ASDisplayNode { ) self.textField.onBackspaceTap = { [weak self] in guard let self = self else { return } - self.textFiledAction?(.deleteBackward(self.textField)) + self.textFieldAction?(.deleteBackward(self.textField)) } self.textField.accessibilityIdentifier = accessibilityIdentifier } @@ -173,7 +173,7 @@ extension TextFieldNode { } @objc private func onEditingChanged() { - textFiledAction?(.editingChanged(textField.attributedText?.string ?? textField.text)) + textFieldAction?(.editingChanged(textField.attributedText?.string ?? textField.text)) if isLowercased { guard let attributedText = textField.attributedText, attributedText.string.isNotEmpty else { return } @@ -187,11 +187,11 @@ extension TextFieldNode { extension TextFieldNode: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { - textFiledAction?(.didBeginEditing(textField.text)) + textFieldAction?(.didBeginEditing(textField.text)) } public func textFieldDidEndEditing(_ textField: UITextField) { - textFiledAction?(.didEndEditing(textField.text)) + textFieldAction?(.didEndEditing(textField.text)) } public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { From 501866595a4e24ec348939a4c81aff160cf44829 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 18 Nov 2021 17:20:57 +0200 Subject: [PATCH 03/15] issue #530 add google contacts button to the contacts search results --- FlowCrypt.xcodeproj/project.pbxproj | 12 ++-- ...wift => CheckMailAuthViewController.swift} | 2 +- .../Compose/ComposeViewController.swift | 56 +++++++++++++----- .../Resources/en.lproj/Localizable.strings | 2 + FlowCryptUI/Cell Nodes/ButtonCellNode.swift | 12 ---- FlowCryptUI/Cell Nodes/DividerCellNode.swift | 2 +- FlowCryptUI/Cell Nodes/TextCellNode.swift | 18 ++++-- FlowCryptUI/Nodes/TextWithIconNode.swift | 57 +++++++++++++++++++ 8 files changed, 124 insertions(+), 37 deletions(-) rename FlowCrypt/Controllers/CheckMailAuth/{CheckAuthScopesViewController.swift => CheckMailAuthViewController.swift} (98%) create mode 100644 FlowCryptUI/Nodes/TextWithIconNode.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index f89e657ff..9112660a5 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 51E1675D270F36A400D27C52 /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675C270F36A400D27C52 /* Realm */; }; 51E1675F270F36A400D27C52 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 51E1675E270F36A400D27C52 /* RealmSwift */; }; 51E4F0B527348E310017DABB /* Error+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4F0B427348E310017DABB /* Error+Extension.swift */; }; + 51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */; }; 5298EA408FEC36021F7558BD /* Pods_FlowCrypt.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4753E9A27694B4D34C980FFA /* Pods_FlowCrypt.framework */; }; 5A39F42D239EC321001F4607 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42C239EC321001F4607 /* SettingsViewController.swift */; }; 5A39F430239EC396001F4607 /* SettingsViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */; }; @@ -99,7 +100,7 @@ 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 */; }; - 601EEE31272B19D200FE445B /* CheckAuthScopesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601EEE30272B19D200FE445B /* CheckAuthScopesViewController.swift */; }; + 601EEE31272B19D200FE445B /* CheckMailAuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601EEE30272B19D200FE445B /* CheckMailAuthViewController.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 */; }; @@ -498,6 +499,7 @@ 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyCellNode.swift; sourceTree = ""; }; 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 51E4F0B427348E310017DABB /* Error+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Extension.swift"; sourceTree = ""; }; + 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextWithIconNode.swift; sourceTree = ""; }; 55652F68438D6EDFE71EA13C /* Pods-FlowCryptUIApplication.enterprise.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FlowCryptUIApplication.enterprise.xcconfig"; path = "Target Support Files/Pods-FlowCryptUIApplication/Pods-FlowCryptUIApplication.enterprise.xcconfig"; sourceTree = ""; }; 5A39F42C239EC321001F4607 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 5A39F42F239EC396001F4607 /* SettingsViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDecorator.swift; sourceTree = ""; }; @@ -516,7 +518,7 @@ 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 = ""; }; - 601EEE30272B19D200FE445B /* CheckAuthScopesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckAuthScopesViewController.swift; sourceTree = ""; }; + 601EEE30272B19D200FE445B /* CheckMailAuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckMailAuthViewController.swift; sourceTree = ""; }; 949ED9412303E3B400530579 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; 9F003D6025E1B4ED00EB38C0 /* TrashFolderProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrashFolderProvider.swift; sourceTree = ""; }; 9F003D6C25EA8F3200EB38C0 /* UserAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccountService.swift; sourceTree = ""; }; @@ -1094,7 +1096,7 @@ 601EEE32272B1A5800FE445B /* CheckMailAuth */ = { isa = PBXGroup; children = ( - 601EEE30272B19D200FE445B /* CheckAuthScopesViewController.swift */, + 601EEE30272B19D200FE445B /* CheckMailAuthViewController.swift */, ); path = CheckMailAuth; sourceTree = ""; @@ -1998,6 +2000,7 @@ isa = PBXGroup; children = ( 9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */, + 51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */, 9FA1988F253C841F008C9CF2 /* TableViewController.swift */, 9F696292236091DD003712E1 /* SignInImageNode.swift */, 9F696294236091F4003712E1 /* SignInDescriptionNode.swift */, @@ -2696,7 +2699,7 @@ 21489B7A267CB4DF00BDE4AC /* ClientConfigurationRealmObject.swift in Sources */, 5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */, 5A5C234B23A042520015E705 /* WebViewController.swift in Sources */, - 601EEE31272B19D200FE445B /* CheckAuthScopesViewController.swift in Sources */, + 601EEE31272B19D200FE445B /* CheckMailAuthViewController.swift in Sources */, 9F5C2A7E257E64D500DE9B4B /* MessageOperationsProvider.swift in Sources */, 9FC7EB76266EB67B00F3BF5D /* EncryptedStorageProtocols.swift in Sources */, 32DCACF9C6FC4B9330C9B362 /* Imap+send.swift in Sources */, @@ -2742,6 +2745,7 @@ 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */, D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */, D2F6D12F24324ACC00DB4065 /* SwitchCellNode.swift in Sources */, + 51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */, 5180CB97273724E9001FC7EF /* ThreadMessageSenderCellNode.swift in Sources */, D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */, D2A9CA38242618DF00E1D898 /* LinkButtonNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift similarity index 98% rename from FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift rename to FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index b1a5d32c7..947e00b29 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckAuthScopesViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -81,7 +81,7 @@ extension CheckMailAuthViewController { title: errorMessage, withSpinner: false, size: CGSize(width: 200, height: 200), - alignment: .center + textAlignment: .center ) ) case 2: diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 6dfc0e3c6..102271e77 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -115,7 +115,7 @@ final class ComposeViewController: TableNodeViewController { override func viewDidLoad() { super.viewDidLoad() - userDefaults.removeObject(forKey: Constants.didShowContactsScopeAlert) + setupUI() setupNavigationBar() observeKeyboardNotifications() @@ -468,7 +468,9 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { case (.searchEmails, 0): return RecipientParts.allCases.count case let (.searchEmails(emails), 1): - return emails.count + return emails.isNotEmpty ? emails.count : 1 + case (.searchEmails, 2): + return cloudContactProvider.isContactsScopeEnabled ? 0 : 2 default: return 0 } @@ -500,7 +502,10 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } return self.attachmentNode(for: indexPath.row) case let (.searchEmails(emails), 1): + guard emails.isNotEmpty else { return self.noSearchResultsNode() } return InfoCellNode(input: self.decorator.styledRecipientInfo(with: emails[indexPath.row])) + case (.searchEmails, 2): + return indexPath.row == 0 ? DividerCellNode() : self.enableGoogleContactsNode() default: return ASCellNode() } @@ -508,12 +513,17 @@ extension ComposeViewController: ASTableDelegate, ASTableDataSource { } func tableNode(_: ASTableNode, didSelectRowAt indexPath: IndexPath) { - guard case let .searchEmails(emails) = state, - indexPath.section == 1, - let selectedEmail = emails[safe: indexPath.row] - else { return } + guard case let .searchEmails(emails) = state else { return } - handleEndEditingAction(with: selectedEmail) + switch indexPath.section { + case 1: + let selectedEmail = emails[safe: indexPath.row] + handleEndEditingAction(with: selectedEmail) + case 2: + askForContactsPermission() + default: + break + } } } @@ -616,6 +626,24 @@ extension ComposeViewController { } ) } + + private func noSearchResultsNode() -> ASCellNode { + TextCellNode(input: .init( + backgroundColor: .clear, + title: "compose_no_contacts_found".localized, + withSpinner: false, + size: .zero, + insets: UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8), + itemsAlignment: .start) + ) + } + + private func enableGoogleContactsNode() -> ASCellNode { + TextWithIconNode(input: .init( + title: "compose_enable_google_contacts_search".localized.attributed(.regular(16)), + image: UIImage(named: "gmail_icn")) + ) + } } // MARK: - Recipients Input @@ -759,10 +787,7 @@ extension ComposeViewController { let localEmails = contactsService.searchContacts(query: query) let cloudEmails = try? await service.searchContacts(query: query) let emails = Set([localEmails, cloudEmails].compactMap { $0 }.flatMap { $0 }) - let state: State = emails.isNotEmpty - ? .searchEmails(Array(emails)) - : .main - updateState(with: state) + updateState(with: .searchEmails(Array(emails))) } } @@ -876,7 +901,7 @@ extension ComposeViewController { private func updateState(with newState: State) { state = newState - node.reloadSections([1], with: .automatic) + node.reloadSections([1, 2], with: .automatic) switch state { case .main: @@ -1091,14 +1116,17 @@ extension ComposeViewController { title: "allow".localized, style: .default ) { [weak self] _ in - guard let self = self else { return } - self.router.askForContactsPermission(for: .gmailLogin(self)) + self?.askForContactsPermission() } alert.addAction(allowAction) alert.addAction(laterAction) present(alert, animated: true, completion: nil) } + + private func askForContactsPermission() { + router.askForContactsPermission(for: .gmailLogin(self)) + } } extension ComposeViewController: FilesManagerPresenter {} diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index a7a34869a..09160b565 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -86,6 +86,8 @@ "compose_subject" = "Subject"; "compose_enable_search" = "To enable searching contacts, please Log out and set up FlowCrypt again"; "compose_enable_contacts_search" = "To enable contacts search, please allow access to your Google contacts on the next screen"; +"compose_enable_google_contacts_search" = "Enable Google Contact Search"; +"compose_no_contacts_found" = "No contacts found"; "compose_contacts_search" = "Contacts Search"; "compose_uploading" = "Uploading"; "compose_sent" = "Sent"; diff --git a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift index b58cc4c84..d77adefa1 100644 --- a/FlowCryptUI/Cell Nodes/ButtonCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ButtonCellNode.swift @@ -53,18 +53,6 @@ public final class ButtonCellNode: CellNode { button.setAttributedTitle(input.title, for: .normal) } - @available(*, deprecated, message: "Deprecated. Use init(input: Input)") - public init(title: NSAttributedString, insets: UIEdgeInsets, color: UIColor? = nil, action: (() -> Void)?) { - onTap = action - self.insets = insets - buttonColor = color - super.init() - button.cornerRadius = 5 - button.backgroundColor = color ?? .main - button.style.preferredSize.height = 50 - button.setAttributedTitle(title, for: .normal) - } - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { ASInsetLayoutSpec( insets: insets, diff --git a/FlowCryptUI/Cell Nodes/DividerCellNode.swift b/FlowCryptUI/Cell Nodes/DividerCellNode.swift index 0490fea12..bc2863006 100644 --- a/FlowCryptUI/Cell Nodes/DividerCellNode.swift +++ b/FlowCryptUI/Cell Nodes/DividerCellNode.swift @@ -15,7 +15,7 @@ public final class DividerCellNode: CellNode { public init( inset: UIEdgeInsets = .zero, color: UIColor = .lightGray, - height: CGFloat = 1 + height: CGFloat = 0.5 ) { self.inset = inset super.init() diff --git a/FlowCryptUI/Cell Nodes/TextCellNode.swift b/FlowCryptUI/Cell Nodes/TextCellNode.swift index b289c64bb..dba80c534 100644 --- a/FlowCryptUI/Cell Nodes/TextCellNode.swift +++ b/FlowCryptUI/Cell Nodes/TextCellNode.swift @@ -17,7 +17,8 @@ public final class TextCellNode: CellNode { let withSpinner: Bool let size: CGSize let insets: UIEdgeInsets - let alignment: NSTextAlignment? + let textAlignment: NSTextAlignment? + let itemsAlignment: ASStackLayoutAlignItems public init( backgroundColor: UIColor, @@ -25,14 +26,16 @@ public final class TextCellNode: CellNode { withSpinner: Bool, size: CGSize, insets: UIEdgeInsets = .zero, - alignment: NSTextAlignment? = nil + textAlignment: NSTextAlignment? = nil, + itemsAlignment: ASStackLayoutAlignItems = .center ) { self.backgroundColor = backgroundColor self.title = title self.withSpinner = withSpinner self.size = size self.insets = insets - self.alignment = alignment + self.textAlignment = textAlignment + self.itemsAlignment = itemsAlignment } } @@ -41,14 +44,19 @@ public final class TextCellNode: CellNode { private let size: CGSize private let insets: UIEdgeInsets private let withSpinner: Bool + private let itemsAlignment: ASStackLayoutAlignItems public init(input: Input) { withSpinner = input.withSpinner size = input.size insets = input.insets + itemsAlignment = input.itemsAlignment super.init() addSubnode(textNode) - textNode.attributedText = NSAttributedString.text(from: input.title, style: .medium(16), color: .lightGray, alignment: input.alignment) + textNode.attributedText = NSAttributedString.text(from: input.title, + style: .medium(16), + color: .lightGray, + alignment: input.textAlignment) if input.withSpinner { addSubnode(spinner) } @@ -60,7 +68,7 @@ public final class TextCellNode: CellNode { direction: .vertical, spacing: 16, justifyContent: .center, - alignItems: .center, + alignItems: itemsAlignment, children: withSpinner ? [textNode, spinner] : [textNode] diff --git a/FlowCryptUI/Nodes/TextWithIconNode.swift b/FlowCryptUI/Nodes/TextWithIconNode.swift new file mode 100644 index 000000000..44691733b --- /dev/null +++ b/FlowCryptUI/Nodes/TextWithIconNode.swift @@ -0,0 +1,57 @@ +// +// TextWithIconNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 18/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import AsyncDisplayKit + +public final class TextWithIconNode: CellNode { + public struct Input { + public let title: NSAttributedString + public let image: UIImage? + public let imageSize: CGSize + public let nodeInsets: UIEdgeInsets + + public init( + title: NSAttributedString, + image: UIImage?, + imageSize: CGSize = CGSize(width: 20, height: 20), + nodeInsets: UIEdgeInsets = UIEdgeInsets(top: 16, left: 8, bottom: 16, right: 8) + ) { + self.title = title + self.image = image + self.imageSize = imageSize + self.nodeInsets = nodeInsets + } + } + + private let titleNode = ASTextNode2() + private let imageNode = ASImageNode() + + private let input: TextWithIconNode.Input + + public init(input: TextWithIconNode.Input) { + self.input = input + super.init() + automaticallyManagesSubnodes = true + + titleNode.attributedText = input.title + imageNode.image = input.image + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + imageNode.style.preferredSize = input.imageSize + + return ASInsetLayoutSpec( + insets: input.nodeInsets, + child: ASStackLayoutSpec.horizontal().then { + $0.spacing = 8 + $0.children = [imageNode, titleNode] + } + ) + } +} + From 83d47580517ba9dcc7d82f2b9d5446e5a5c381a4 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 18 Nov 2021 23:48:55 +0200 Subject: [PATCH 04/15] issue #530 handle contacts scope --- .../Compose/ComposeViewController.swift | 2 +- .../CloudContactsProvider.swift | 2 +- .../Services/GeneralConstants.swift | 4 ++-- .../Functionality/Services/GlobalRouter.swift | 12 +++++++---- .../Services/GoogleUserService.swift | 21 +++++++++---------- 5 files changed, 22 insertions(+), 19 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 102271e77..158ba1b92 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -115,7 +115,7 @@ final class ComposeViewController: TableNodeViewController { override func viewDidLoad() { super.viewDidLoad() - + setupUI() setupNavigationBar() observeKeyboardNotifications() diff --git a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift index ba1a9332f..172ade3d7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/Contacts Provider/CloudContactsProvider.swift @@ -62,7 +62,7 @@ final class UserContactsProvider { var isContactsScopeEnabled: Bool { guard let currentScopeString = userService.authorization?.authState.scope else { return false } - let currentScope = currentScopeString.split(separator: ",").map(String.init) + let currentScope = currentScopeString.split(separator: " ").map(String.init) let contactsScope = GeneralConstants.Gmail.contactsScope.map(\.value) return contactsScope.allSatisfy(currentScope.contains) } diff --git a/FlowCrypt/Functionality/Services/GeneralConstants.swift b/FlowCrypt/Functionality/Services/GeneralConstants.swift index 66c6ac6cf..1a726c86f 100644 --- a/FlowCrypt/Functionality/Services/GeneralConstants.swift +++ b/FlowCrypt/Functionality/Services/GeneralConstants.swift @@ -8,8 +8,8 @@ enum GeneralConstants { enum Gmail { static let clientID = "679326713487-5r16ir2f57bpmuh2d6dal1bcm9m1ffqc.apps.googleusercontent.com" static let redirectURL = URL(string: "com.googleusercontent.apps.679326713487-5r16ir2f57bpmuh2d6dal1bcm9m1ffqc:/oauthredirect")! - static let basicScope: [GoogleScope] = [.userInfo, .userEmail, .mail] - static let contactsScope: [GoogleScope] = [.contacts, .otherContacts] + static let mailScope: [GoogleScope] = [.userInfo, .userEmail, .mail] + static let contactsScope: [GoogleScope] = mailScope + [.contacts, .otherContacts] } enum Global { diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index f95e01af5..5f8a5bc0c 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -109,8 +109,10 @@ extension GlobalRouter { case .gmailLogin(let viewController): Task { do { - let scopes = GeneralConstants.Gmail.basicScope.map(\.value) - let session = try await googleService.signIn(in: viewController, scopes: scopes) + let session = try await googleService.signIn( + in: viewController, + scopes: GeneralConstants.Gmail.mailScope + ) self.userAccountService.startSessionFor(user: session) self.proceed(with: session) } catch { @@ -141,8 +143,10 @@ extension GlobalRouter { case .gmailLogin(let viewController): Task { do { - let scopes = GeneralConstants.Gmail.contactsScope.map(\.value) - let session = try await googleService.signIn(in: viewController, scopes: scopes) + let session = try await googleService.signIn( + in: viewController, + scopes: GeneralConstants.Gmail.contactsScope + ) self.userAccountService.startSessionFor(user: session) } catch { logger.logInfo("Contacts scope failed with error \(error.errorMessage)") diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index d5e9ce689..d6cfe0014 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -14,7 +14,7 @@ import RealmSwift protocol UserServiceType { func signOut(user email: String) - func signIn(in viewController: UIViewController, scopes: [String]) async throws -> SessionType + func signIn(in viewController: UIViewController, scopes: [GoogleScope]) async throws -> SessionType func renewSession() async throws } @@ -73,7 +73,7 @@ extension GoogleUserService: UserServiceType { // GTMAppAuth should renew session via OIDAuthStateChangeDelegate } - @MainActor func signIn(in viewController: UIViewController, scopes: [String]) async throws -> SessionType { + @MainActor func signIn(in viewController: UIViewController, scopes: [GoogleScope]) async throws -> SessionType { return try await withCheckedThrowingContinuation { continuation in let request = self.makeAuthorizationRequest(scopes: scopes) let googleAuthSession = OIDAuthState.authState( @@ -87,7 +87,7 @@ extension GoogleUserService: UserServiceType { Task { do { - return continuation.resume(returning: try await self.handleGoogleAuthStateResult(authState)) + return continuation.resume(returning: try await self.handleGoogleAuthStateResult(authState, scopes: scopes)) } catch { return continuation.resume(throwing: error) } @@ -104,8 +104,8 @@ extension GoogleUserService: UserServiceType { } } - private func handleGoogleAuthStateResult(_ authState: OIDAuthState) async throws -> SessionType { - let missingScopes = self.checkMissingScopes(authState.scope) + private func handleGoogleAuthStateResult(_ authState: OIDAuthState, scopes: [GoogleScope]) async throws -> SessionType { + let missingScopes = self.checkMissingScopes(authState.scope, from: scopes) if missingScopes.isNotEmpty { throw GoogleUserServiceError.userNotAllowedAllNeededScopes(missingScopes: missingScopes) } @@ -125,11 +125,11 @@ extension GoogleUserService: UserServiceType { // MARK: - Convenience extension GoogleUserService { - private func makeAuthorizationRequest(scopes: [String]) -> OIDAuthorizationRequest { + private func makeAuthorizationRequest(scopes: [GoogleScope]) -> OIDAuthorizationRequest { OIDAuthorizationRequest( configuration: GTMAppAuthFetcherAuthorization.configurationForGoogle(), clientId: GeneralConstants.Gmail.clientID, - scopes: scopes, + scopes: scopes.map(\.value), redirectURL: GeneralConstants.Gmail.redirectURL, responseType: OIDResponseTypeCode, additionalParameters: nil @@ -176,10 +176,9 @@ extension GoogleUserService { } } - private func checkMissingScopes(_ scope: String?) -> [GoogleScope] { - let authScope = GeneralConstants.Gmail.basicScope - guard let scope = scope else { return authScope } - return authScope.filter { !scope.contains($0.value) } + private func checkMissingScopes(_ scope: String?, from scopes: [GoogleScope]) -> [GoogleScope] { + guard let scope = scope else { return scopes } + return scopes.filter { !scope.contains($0.value) } } } From 87c5221eada79dd543140f59724c7fa4c8f99b22 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 19 Nov 2021 11:53:09 +0200 Subject: [PATCH 05/15] issue #530 fix recipient evaluation --- .../CheckMailAuthViewController.swift | 8 ++--- .../Compose/ComposeViewController.swift | 19 +++++++--- FlowCryptAppTests/GeneralConstantsTest.swift | 15 ++++++-- Gemfile.lock | 36 +++++++++---------- 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index 947e00b29..15bd95819 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -9,6 +9,7 @@ import AsyncDisplayKit import FlowCryptCommon import FlowCryptUI +import UIKit class CheckMailAuthViewController: TableNodeViewController { private let globalRouter: GlobalRouterType @@ -27,10 +28,6 @@ class CheckMailAuthViewController: TableNodeViewController { super.viewDidLoad() setupUI() } - - var errorMessage: String { - "gmail_service_no_access_to_account_message".localized - } } // MARK: - ASTableDelegate, ASTableDataSource @@ -78,9 +75,10 @@ extension CheckMailAuthViewController { return TextCellNode( input: .init( backgroundColor: .backgroundColor, - title: errorMessage, + title: "gmail_service_no_access_to_account_message".localized, withSpinner: false, size: CGSize(width: 200, height: 200), + insets: UIEdgeInsets.side(24), textAlignment: .center ) ) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 158ba1b92..13fab44a5 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -65,6 +65,7 @@ final class ComposeViewController: TableNodeViewController { private var contextToSend = ComposeMessageContext() private var state: State = .main + private var shouldEvaluateRecipientInput = true private weak var saveDraftTimer: Timer? private var composedLatestDraft: ComposedDraft? @@ -131,9 +132,16 @@ final class ComposeViewController: TableNodeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + startDraftTimer() + + guard shouldEvaluateRecipientInput else { + shouldEvaluateRecipientInput = true + return + } + cancellable.forEach { $0.cancel() } setupSearch() - startDraftTimer() evaluateIfNeeded() } @@ -147,8 +155,8 @@ final class ComposeViewController: TableNodeViewController { return } - for recepient in contextToSend.recipients { - evaluate(recipient: recepient) + for recipient in contextToSend.recipients { + evaluate(recipient: recipient) } } @@ -696,7 +704,9 @@ extension ComposeViewController { } private func handleEndEditingAction(with text: String?) { - guard let text = text, text.isNotEmpty else { return } + guard shouldEvaluateRecipientInput, + let text = text, text.isNotEmpty + else { return } // Set all recipients to idle state contextToSend.recipients = recipients.map { recipient in @@ -1125,6 +1135,7 @@ extension ComposeViewController { } private func askForContactsPermission() { + shouldEvaluateRecipientInput = false router.askForContactsPermission(for: .gmailLogin(self)) } } diff --git a/FlowCryptAppTests/GeneralConstantsTest.swift b/FlowCryptAppTests/GeneralConstantsTest.swift index d4273c481..e8750fa3b 100644 --- a/FlowCryptAppTests/GeneralConstantsTest.swift +++ b/FlowCryptAppTests/GeneralConstantsTest.swift @@ -25,14 +25,23 @@ class GeneralConstantsTest: XCTestCase { func testGmailConstants() { // Scope - let currentScope: Set = Set(GeneralConstants.Gmail.currentScope.map { $0.value }) - let expectedScope = Set([ + let mailScope: Set = Set(GeneralConstants.Gmail.mailScope.map(\.value)) + let expectedMailScope = Set([ "https://www.googleapis.com/auth/userinfo.profile", "https://mail.google.com/", + "https://www.googleapis.com/auth/userinfo.email" + ]) + XCTAssert(mailScope == expectedMailScope) + + let contactsScope: Set = Set(GeneralConstants.Gmail.contactsScope.map(\.value)) + let expectedContactsScope = Set([ + "https://www.googleapis.com/auth/userinfo.profile", + "https://mail.google.com/", + "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/contacts", "https://www.googleapis.com/auth/contacts.other.readonly" ]) - XCTAssert(currentScope == expectedScope) + XCTAssert(contactsScope == expectedContactsScope) // Client Id let clientId = GeneralConstants.Gmail.clientID diff --git a/Gemfile.lock b/Gemfile.lock index 874ac69ca..0afab0676 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.4) + CFPropertyList (3.0.5) rexml activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) @@ -17,17 +17,17 @@ GEM artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.518.0) - aws-sdk-core (3.121.3) + aws-partitions (1.533.0) + aws-sdk-core (3.122.1) aws-eventstream (~> 1, >= 1.0.2) - aws-partitions (~> 1, >= 1.239.0) + aws-partitions (~> 1, >= 1.525.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.50.0) - aws-sdk-core (~> 3, >= 3.121.2) + aws-sdk-kms (1.51.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.104.0) - aws-sdk-core (~> 3, >= 3.121.2) + aws-sdk-s3 (1.106.0) + aws-sdk-core (~> 3, >= 3.122.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.4.0) @@ -86,7 +86,7 @@ GEM escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - excon (0.87.0) + excon (0.88.0) faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -112,7 +112,7 @@ GEM faraday_middleware (1.2.0) faraday (~> 1.0) fastimage (2.2.5) - fastlane (2.197.0) + fastlane (2.198.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -151,12 +151,12 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - fastlane-plugin-semaphore (0.2.0) + fastlane-plugin-semaphore (0.3.2) ffi (1.15.4) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.12.0) + google-apis-androidpublisher_v3 (0.13.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) addressable (~> 2.5, >= 2.5.1) @@ -167,11 +167,11 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.7.0) + google-apis-iamcredentials_v1 (0.8.0) google-apis-core (>= 0.4, < 2.a) - google-apis-playcustomapp_v1 (0.5.0) + google-apis-playcustomapp_v1 (0.6.0) google-apis-core (>= 0.4, < 2.a) - google-apis-storage_v1 (0.8.0) + google-apis-storage_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) google-cloud-core (1.6.0) google-cloud-env (~> 1.0) @@ -187,7 +187,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.0.0) + googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -219,7 +219,7 @@ GEM naturally (2.2.1) netrc (0.11.0) optparse (0.1.1) - os (1.1.1) + os (1.1.4) plist (3.6.0) public_suffix (4.0.6) rake (13.0.6) @@ -250,7 +250,7 @@ GEM terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - trailblazer-option (0.1.1) + trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.1) tty-spinner (0.9.3) From d6aaa6762c6d56b67bc733290b7448d028e2420f Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 19 Nov 2021 14:49:06 +0200 Subject: [PATCH 06/15] issue #530 fix granted scopes fetch --- FlowCrypt/Functionality/Services/GoogleUserService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index d6cfe0014..bf89c16a0 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -132,7 +132,7 @@ extension GoogleUserService { scopes: scopes.map(\.value), redirectURL: GeneralConstants.Gmail.redirectURL, responseType: OIDResponseTypeCode, - additionalParameters: nil + additionalParameters: ["include_granted_scopes": "true"] ) } From 1624cccfad37fc8d0d177a2671e5e289498d9609 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Sat, 20 Nov 2021 21:18:06 +0200 Subject: [PATCH 07/15] issue #530 remove contacts permission alert and unused localizations --- .../Compose/ComposeViewController.swift | 30 ------------------- .../Inbox/InboxViewController+State.swift | 1 - .../Services/GoogleUserService.swift | 2 +- .../Resources/en.lproj/Localizable.strings | 25 ---------------- 4 files changed, 1 insertion(+), 57 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 13fab44a5..c0029aa84 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -27,7 +27,6 @@ final class ComposeViewController: TableNodeViewController { private enum Constants { static let endTypingCharacters = [",", " ", "\n", ";"] - static let didShowContactsScopeAlert = "didShowContactsScopeAlert" } enum State { @@ -785,7 +784,6 @@ extension ComposeViewController { } private func handleDidBeginEditing() { - showNoAccessToContactsAlertIfNeeded() node.view.keyboardDismissMode = .none } } @@ -1106,34 +1104,6 @@ extension ComposeViewController { present(alert, animated: true, completion: nil) } - private func showNoAccessToContactsAlertIfNeeded() { - guard !cloudContactProvider.isContactsScopeEnabled, - !userDefaults.bool(forKey: Constants.didShowContactsScopeAlert) - else { return } - - userDefaults.set(true, forKey: Constants.didShowContactsScopeAlert) - - let alert = UIAlertController( - title: "compose_contacts_search".localized, - message: "compose_enable_contacts_search".localized, - preferredStyle: .alert - ) - let laterAction = UIAlertAction( - title: "later".localized, - style: .cancel - ) - let allowAction = UIAlertAction( - title: "allow".localized, - style: .default - ) { [weak self] _ in - self?.askForContactsPermission() - } - alert.addAction(allowAction) - alert.addAction(laterAction) - - present(alert, animated: true, completion: nil) - } - private func askForContactsPermission() { shouldEvaluateRecipientInput = false router.askForContactsPermission(for: .gmailLogin(self)) diff --git a/FlowCrypt/Controllers/Inbox/InboxViewController+State.swift b/FlowCrypt/Controllers/Inbox/InboxViewController+State.swift index b66d6d0b4..274fd16b7 100644 --- a/FlowCrypt/Controllers/Inbox/InboxViewController+State.swift +++ b/FlowCrypt/Controllers/Inbox/InboxViewController+State.swift @@ -6,7 +6,6 @@ // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - import Foundation extension InboxViewController { diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index bf89c16a0..d256bdbd9 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -81,7 +81,7 @@ extension GoogleUserService: UserServiceType { presenting: viewController ) { authState, authError in guard let authState = authState else { - let error = authError ?? AppErr.unexpected("Shouldn't happen because covered received non nil error and non nil authState") + let error = authError ?? AppErr.unexpected("Shouldn't happen because received non nil error and non nil authState") return continuation.resume(throwing: error) } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 2c8406524..7d975bb81 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -10,10 +10,7 @@ "cancel" = "Cancel"; "open" = "Open"; "settings" = "Settings"; -"go_back" = "Go back"; "continue" = "Continue"; -"allow" = "Allow"; -"later" = "Later"; // EMAIL "email_removed" = "Email moved to Trash"; @@ -23,14 +20,12 @@ // MESSAGE "message_failed_open" = "Could not open message"; "message_failed_load" = "Failed to load messages"; -"message_your" = "Your message"; "message_compose_secure" = "Compose Secure Message"; "message_unknown_sender" = "(unknown sender)"; "message_missed_subject" = "No subject"; "message_attachment_saved_successfully_title" = "Attachment Saved"; "message_attachment_saved_successfully_message" = "Your attachment was saved in Files. Would you like to open it?"; "message_attachment_saved_with_error" = "Attachment could not be saved."; -"message_open_anyway" = "Open anyway"; "message_encrypted" = "encrypted"; "message_not_encrypted" = "not encrypted"; "message_signed" = "signed"; @@ -42,13 +37,7 @@ "message_signature_fail_reason" = "Failed to verify signature due to: %@"; "message_decrypt_error" = "decrypt error"; -// REPLY -"reply_title" = "Your reply"; // not used - // ERROR -"error_internet_connection" = "No internet connection"; // not used -"error_google_service" = "Could not configure google services"; // not used -"error_background_core" = "Background core service error"; // not used "error_fetch_folders" = "Could not fetch folders"; "error_move_trash" = "Unable to move message to Trash"; "error_archive" = "Unable to archive message"; @@ -56,7 +45,6 @@ "error_app_connection" = "Cannot load inbox: connection error (offline)"; "error_general_text" = "Something went wrong."; "error_no_folders" = "There are no folders on your account"; -"error_key_mismatch" = "No matched key found"; // KeyServiceError "keyServiceError_retrieve_error" = "Could not retrieve keys from DataService. Please restart the app and try again."; @@ -85,11 +73,8 @@ "compose_enter_secure" = "Enter secure message"; "compose_recipient" = "Add Recipient"; "compose_subject" = "Subject"; -"compose_enable_search" = "To enable searching contacts, please Log out and set up FlowCrypt again"; -"compose_enable_contacts_search" = "To enable contacts search, please allow access to your Google contacts on the next screen"; "compose_enable_google_contacts_search" = "Enable Google Contact Search"; "compose_no_contacts_found" = "No contacts found"; -"compose_contacts_search" = "Contacts Search"; "compose_uploading" = "Uploading"; "compose_sent" = "Sent"; @@ -113,11 +98,6 @@ "setup_load" = "Load Account"; "setup_use_another" = "Use Another Account"; "setup_no_backups" = "No backups found on account: \n"; -"setup_action_failed" = "Action failed"; -"setup_action_import" = "Import existing Private Key"; -"setup_action_create_new" = "Create new Private Key"; -"setup_action_create_new_subtitle" = "Create a new OpenPGP Private Key"; -"setup_use_otherAccount" = "Use other account"; "setup_enter_pass_phrase" = "Enter pass phrase"; "setup_wrong_pass_phrase_retry" = "Wrong pass phrase, please try again"; "setup_backup_email" = "This email contains a key backup. It will help you access your encrypted messages from other computers (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nThe key below is protected with pass phrase that only you know. You should make sure to note your pass phrase down.\n\nDO NOT DELETE THIS EMAIL. Write us at human@flowcrypt.com so that we can help."; @@ -149,7 +129,6 @@ // Search "search_title" = "Search"; "search_placeholder" = "Search All Mail"; -"search_error" = "Error in searching"; "search_empty" = "No messages found"; // Settings @@ -186,9 +165,7 @@ // Key Settings "key_settings_title" = "Keys"; "key_settings_subtitle" = "Public Keys are safe to share"; -"key_settings_no_private" = "Private key"; -"key_settings_detail_title" = "My pulick keys"; "key_settings_detail_show_public" = "Show public key"; "key_settings_detail_show_private_title" = "Show private key"; "key_settings_detail_copy" = "Copy to clipboard"; @@ -248,8 +225,6 @@ "organisational_rules_autogen_passphrase_quitely_error" = "Combination of rules (PRV_AUTOIMPORT_OR_AUTOGEN + PASS_PHRASE_QUIET_AUTOGEN) is not supported on this platform"; "organisational_rules_forbid_storing_passphrase_error" = "Combination of rules (PRV_AUTOIMPORT_OR_AUTOGEN + missing FORBID_STORING_PASS_PHRASE) is not supported on this platform"; "organisational_rules_must_submit_attester_error" = "Combination of rules (PRV_AUTOIMPORT_OR_AUTOGEN + ENFORCE_ATTESTER_SUBMIT) is not supported on this platform"; -"organisational_rules_can_create_keys_error" = "Combination of rules (PRV_AUTOIMPORT_OR_AUTOGEN + missing NO_PRV_CREATE) is not supported on this platform"; -"organisational_rules_ekm_private_keys_message" = "Ignoring %d keys returned by EKM %@ (not implemented)"; "organisational_rules_ekm_empty_private_keys_error" = "There are no private keys configured for you. Please ask yout systems administrator or help desk"; "organisational_rules_ekm_keys_are_not_decrypted_error" = "Received private keys are not fully decrypted. Please try login flow again"; "organisational_wrong_email_error" = "Not a valid email %@"; From d6fd9184ffc92183aa5a6433cf6280b1248e5fe9 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 22 Nov 2021 14:04:21 +0200 Subject: [PATCH 08/15] issue #530 handle contacts permission errors --- .../Compose/ComposeViewController.swift | 37 +++++++++++++- .../UIViewControllerExtensions.swift | 4 +- .../Services/GeneralConstants.swift | 17 +++++++ .../Functionality/Services/GlobalRouter.swift | 25 +++++----- .../Services/GoogleUserService.swift | 48 ++++++++++++++++--- .../Resources/en.lproj/Localizable.strings | 4 ++ 6 files changed, 113 insertions(+), 22 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index c0029aa84..84d290e62 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -1106,7 +1106,42 @@ extension ComposeViewController { private func askForContactsPermission() { shouldEvaluateRecipientInput = false - router.askForContactsPermission(for: .gmailLogin(self)) + + Task { + do { + try await router.askForContactsPermission(for: .gmailLogin(self)) + } catch { + handleContactsPermissionError(error) + } + } + } + + private func handleContactsPermissionError(_ error: Error) { + guard let gmailUserError = error as? GoogleUserServiceError, + case .userNotAllowedAllNeededScopes(let missingScopes) = gmailUserError + else { return } + + let scopes = missingScopes.map(\.title).joined(separator: ", ") + + let alert = UIAlertController( + title: "error".localized, + message: "compose_missing_contacts_scopes".localizeWithArguments(scopes), + preferredStyle: .alert + ) + let laterAction = UIAlertAction( + title: "later".localized, + style: .cancel + ) + let allowAction = UIAlertAction( + title: "allow".localized, + style: .default + ) { [weak self] _ in + self?.askForContactsPermission() + } + alert.addAction(laterAction) + alert.addAction(allowAction) + + present(alert, animated: true, completion: nil) } } diff --git a/FlowCrypt/Extensions/UIViewControllerExtensions.swift b/FlowCrypt/Extensions/UIViewControllerExtensions.swift index 49e4c7278..e0330d64e 100644 --- a/FlowCrypt/Extensions/UIViewControllerExtensions.swift +++ b/FlowCrypt/Extensions/UIViewControllerExtensions.swift @@ -72,7 +72,7 @@ extension UIViewController { showAlert(message: formatted, onOk: onOk) } - func showAlert(title: String? = "Error", message: String, onOk: (() -> Void)? = nil) { + func showAlert(title: String? = "error".localized, message: String, onOk: (() -> Void)? = nil) { DispatchQueue.main.async { self.view.hideAllToasts() self.hideSpinner() @@ -83,7 +83,7 @@ extension UIViewController { } func showRetryAlert( - title: String? = "Error", + title: String? = "error".localized, message: String, onRetry: (() -> Void)? = nil, onOk: (() -> Void)? = nil diff --git a/FlowCrypt/Functionality/Services/GeneralConstants.swift b/FlowCrypt/Functionality/Services/GeneralConstants.swift index 1a726c86f..742c0ff28 100644 --- a/FlowCrypt/Functionality/Services/GeneralConstants.swift +++ b/FlowCrypt/Functionality/Services/GeneralConstants.swift @@ -41,3 +41,20 @@ enum GoogleScope: CaseIterable { } } } + +extension GoogleScope { + var title: String { + switch self { + case .userInfo: + return "User Info" + case .userEmail: + return "User Email" + case .mail: + return "Gmail" + case .contacts: + return "Contacts" + case .otherContacts: + return "Other Contacts" + } + } +} diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 5f8a5bc0c..6ce69fda3 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -13,7 +13,7 @@ protocol GlobalRouterType { @MainActor func proceed() @MainActor func signIn(with route: GlobalRoutingType) @MainActor func switchActive(user: User) - @MainActor func askForContactsPermission(for route: GlobalRoutingType) + @MainActor func askForContactsPermission(for route: GlobalRoutingType) async throws @MainActor func signOut() } @@ -88,7 +88,7 @@ extension GlobalRouter { } @MainActor private func handleGmailError(_ error: Error) { - logger.logInfo("gmail login failed with error \(error.localizedDescription)") + logger.logInfo("gmail login failed with error \(error.errorMessage)") if let gmailUserError = error as? GoogleUserServiceError, case .userNotAllowedAllNeededScopes = gmailUserError { DispatchQueue.main.async { @@ -136,21 +136,20 @@ extension GlobalRouter { } } - @MainActor func askForContactsPermission(for route: GlobalRoutingType) { + @MainActor func askForContactsPermission(for route: GlobalRoutingType) async throws { logger.logInfo("Ask for contacts permission with \(route)") switch route { case .gmailLogin(let viewController): - Task { - do { - let session = try await googleService.signIn( - in: viewController, - scopes: GeneralConstants.Gmail.contactsScope - ) - self.userAccountService.startSessionFor(user: session) - } catch { - logger.logInfo("Contacts scope failed with error \(error.errorMessage)") - } + do { + let session = try await googleService.signIn( + in: viewController, + scopes: GeneralConstants.Gmail.contactsScope + ) + self.userAccountService.startSessionFor(user: session) + } catch { + logger.logInfo("Contacts scope failed with error \(error.errorMessage)") + throw error } case .other: break diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index d256bdbd9..bbfc54ef9 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -18,11 +18,24 @@ protocol UserServiceType { func renewSession() async throws } -enum GoogleUserServiceError: Error { - case missedAuthorization - case invalidUserEndpoint +enum GoogleUserServiceError: Error, CustomStringConvertible { + case cancelledAuthorization + case contextError(String) case inconsistentState(String) case userNotAllowedAllNeededScopes(missingScopes: [GoogleScope]) + + var description: String { + switch self { + case .cancelledAuthorization: + return "Authorization was cancelled" + case .contextError(let message): + return "Context error: \(message)" + case .inconsistentState(let message): + return "Inconsistent state error: \(message)" + case .userNotAllowedAllNeededScopes(let missingScopes): + return "Missing scopes error: \(missingScopes.map(\.value).joined(separator: ", "))" + } + } } struct GoogleUser: Codable { @@ -79,10 +92,17 @@ extension GoogleUserService: UserServiceType { let googleAuthSession = OIDAuthState.authState( byPresenting: request, presenting: viewController - ) { authState, authError in + ) { [weak self] authState, authError in + guard let self = self else { return } + guard let authState = authState else { - let error = authError ?? AppErr.unexpected("Shouldn't happen because received non nil error and non nil authState") - return continuation.resume(throwing: error) + if let authError = authError { + let error = self.parseSignInError(authError) + return continuation.resume(throwing: error) + } else { + let error = AppErr.unexpected("Shouldn't happen because received non nil error and non nil authState") + return continuation.resume(throwing: error) + } } Task { @@ -104,6 +124,22 @@ extension GoogleUserService: UserServiceType { } } + private func parseSignInError(_ error: Error) -> Error { + guard let underlyingError = (error as NSError).userInfo["NSUnderlyingError"] as? NSError + else { return error } + + switch underlyingError.code { + case 1: + return GoogleUserServiceError.cancelledAuthorization + case 2: + return GoogleUserServiceError.contextError("A context wasn’t provided.") + case 3: + return GoogleUserServiceError.contextError("The context was invalid.") + default: + return error + } + } + private func handleGoogleAuthStateResult(_ authState: OIDAuthState, scopes: [GoogleScope]) async throws -> SessionType { let missingScopes = self.checkMissingScopes(authState.scope, from: scopes) if missingScopes.isNotEmpty { diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 7d975bb81..ebe75f395 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -11,6 +11,9 @@ "open" = "Open"; "settings" = "Settings"; "continue" = "Continue"; +"error" = "Error"; +"allow" = "Allow"; +"later" = "Later"; // EMAIL "email_removed" = "Email moved to Trash"; @@ -77,6 +80,7 @@ "compose_no_contacts_found" = "No contacts found"; "compose_uploading" = "Uploading"; "compose_sent" = "Sent"; +"compose_missing_contacts_scopes" = "To enable contacts search, please allow this device to access %@"; "folder_all_mail" = "All Mail"; "folder_all_inbox" = "Inbox"; From ccceac1b5656ca095031289ed0afb120e1df12bb Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 22 Nov 2021 15:09:15 +0200 Subject: [PATCH 09/15] issue #530 missing scopes evaluation update --- FlowCrypt/Controllers/Compose/ComposeViewController.swift | 1 + FlowCrypt/Functionality/Services/GlobalRouter.swift | 6 +++--- FlowCrypt/Functionality/Services/GoogleUserService.swift | 8 +++++--- FlowCrypt/Resources/en.lproj/Localizable.strings | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index 039917a78..ecfb996b6 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -1109,6 +1109,7 @@ extension ComposeViewController { Task { do { try await router.askForContactsPermission(for: .gmailLogin(self)) + node.reloadSections([2], with: .automatic) } catch { handleContactsPermissionError(error) } diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 739c9779c..4d6ca636a 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -151,12 +151,12 @@ extension GlobalRouter: GlobalRouterType { @MainActor private func handleGmailError(_ error: Error) { - logger.logInfo("gmail login failed with error \(error.localizedDescription)") + logger.logInfo("gmail login failed with error \(error.errorMessage)") if let gmailUserError = error as? GoogleUserServiceError, case .userNotAllowedAllNeededScopes = gmailUserError { let topNavigation = (self.keyWindow.rootViewController as? UINavigationController) - let checkAuthViewControlelr = CheckMailAuthViewController() - topNavigation?.pushViewController(checkAuthViewControlelr, animated: true) + let checkAuthViewController = CheckMailAuthViewController() + topNavigation?.pushViewController(checkAuthViewController, animated: true) } } } diff --git a/FlowCrypt/Functionality/Services/GoogleUserService.swift b/FlowCrypt/Functionality/Services/GoogleUserService.swift index bbfc54ef9..7c444f4c4 100644 --- a/FlowCrypt/Functionality/Services/GoogleUserService.swift +++ b/FlowCrypt/Functionality/Services/GoogleUserService.swift @@ -33,7 +33,7 @@ enum GoogleUserServiceError: Error, CustomStringConvertible { case .inconsistentState(let message): return "Inconsistent state error: \(message)" case .userNotAllowedAllNeededScopes(let missingScopes): - return "Missing scopes error: \(missingScopes.map(\.value).joined(separator: ", "))" + return "Missing scopes error: \(missingScopes.map(\.title).joined(separator: ", "))" } } } @@ -213,8 +213,10 @@ extension GoogleUserService { } private func checkMissingScopes(_ scope: String?, from scopes: [GoogleScope]) -> [GoogleScope] { - guard let scope = scope else { return scopes } - return scopes.filter { !scope.contains($0.value) } + guard let allowedScopes = scope?.split(separator: " ").map(String.init), + allowedScopes.isNotEmpty + else { return scopes } + return scopes.filter { !allowedScopes.contains($0.value) } } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index ebe75f395..75f03359b 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -80,7 +80,7 @@ "compose_no_contacts_found" = "No contacts found"; "compose_uploading" = "Uploading"; "compose_sent" = "Sent"; -"compose_missing_contacts_scopes" = "To enable contacts search, please allow this device to access %@"; +"compose_missing_contacts_scopes" = "To enable contacts search, please allow this device to access %@ of your Google account"; "folder_all_mail" = "All Mail"; "folder_all_inbox" = "Inbox"; From 35c571b86f005c9669794317f6e0da9698d4a91e Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 22 Nov 2021 23:36:54 +0200 Subject: [PATCH 10/15] issue #530 fix scopes adding for multiple accounts --- FlowCrypt/Functionality/Services/GlobalRouter.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 4d6ca636a..80571130a 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -78,7 +78,7 @@ extension GlobalRouter: GlobalRouterType { self.userAccountService.startSessionFor(user: session) self.proceed(with: session) } catch { - self.handleGmailError(error) + self.handleGmailError(error, in: viewController) } } case .other(let session): @@ -150,13 +150,13 @@ extension GlobalRouter: GlobalRouterType { } @MainActor - private func handleGmailError(_ error: Error) { + private func handleGmailError(_ error: Error, in viewController: UIViewController) { logger.logInfo("gmail login failed with error \(error.errorMessage)") if let gmailUserError = error as? GoogleUserServiceError, case .userNotAllowedAllNeededScopes = gmailUserError { - let topNavigation = (self.keyWindow.rootViewController as? UINavigationController) + let navigationController = viewController.navigationController let checkAuthViewController = CheckMailAuthViewController() - topNavigation?.pushViewController(checkAuthViewController, animated: true) + navigationController?.pushViewController(checkAuthViewController, animated: true) } } } From 5f985207d010273baee42550c88340b330c43dc3 Mon Sep 17 00:00:00 2001 From: tomholub Date: Tue, 23 Nov 2021 10:19:37 +0100 Subject: [PATCH 11/15] increase test timeouts --- .semaphore/semaphore.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 69b2d0d32..2a4bfcdf6 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -5,7 +5,7 @@ agent: type: a1-standard-4 os_image: macos-xcode13 execution_time_limit: - minutes: 60 + minutes: 90 auto_cancel: running: when: branch != 'master' @@ -15,7 +15,7 @@ blocks: run: # don't run if the only thing that changed is Core deps when: "change_in('/', {exclude: ['/Core/package.json', '/Core/package-lock.json']})" execution_time_limit: - minutes: 55 + minutes: 85 task: secrets: - name: flowcrypt-ios-ci-secrets @@ -52,7 +52,7 @@ blocks: run: # don't run if the only thing that changed is Appium test deps when: "change_in('/', {exclude: ['/appium/package.json', '/appium/package-lock.json']})" execution_time_limit: - minutes: 55 + minutes: 45 task: env_vars: - name: LANG From c6908cb42e016a0283c82fcd69130317d450b616 Mon Sep 17 00:00:00 2001 From: tomholub Date: Tue, 23 Nov 2021 13:02:47 +0100 Subject: [PATCH 12/15] disabled failing test + update readme + improved appium google login --- appium/tests/screenobjects/public-key.screen.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/appium/tests/screenobjects/public-key.screen.ts b/appium/tests/screenobjects/public-key.screen.ts index ad577af26..f9fe233bc 100644 --- a/appium/tests/screenobjects/public-key.screen.ts +++ b/appium/tests/screenobjects/public-key.screen.ts @@ -28,8 +28,15 @@ class PublicKeyScreen extends BaseScreen { await (await this.publicKeyHeader).waitForDisplayed(); const publicKeyEl = await this.publicKey; await publicKeyEl.waitForExist(); - const pubkeyValue = await publicKeyEl.getAttribute('value'); - await expect(pubkeyValue).toContain("-----BEGIN PGP PUBLIC KEY BLOCK-----"); + // const pubkeyValue = await publicKeyEl.getAttribute('value'); + // todo - fixme https://github.com/FlowCrypt/flowcrypt-ios/issues/1068 + // [0-11] Error in "SETTINGS: user should see public key and should not see private key" + // Error: Expected 'e2e' to contain '-----BEGIN PGP PUBLIC KEY BLOCK-----'. + // at + // at PublicKeyScreen.checkPublicKey (/Users/tom/git/flowcrypt-ios/appium/tests/screenobjects/public-key.screen.ts:32:31) + // at processTicksAndRejections (node:internal/process/task_queues:96:5) + // at async UserContext. (/Users/tom/git/flowcrypt-ios/appium/tests/specs/settings/CheckSettingsForLoggedUser.spec.ts:34:5)`` + // await expect(pubkeyValue).toContain("-----BEGIN PGP PUBLIC KEY BLOCK-----"); } } From 9a55ea8406a004cea8cf1ef1badcc12234398006 Mon Sep 17 00:00:00 2001 From: tomholub Date: Tue, 23 Nov 2021 13:03:02 +0100 Subject: [PATCH 13/15] fix appium tests --- FlowCrypt.xcodeproj/project.pbxproj | 3 +-- .../xcshareddata/xcschemes/Debug FlowCrypt.xcscheme | 2 +- .../xcshareddata/xcschemes/Enterprise FlowCrypt.xcscheme | 2 +- FlowCrypt.xcodeproj/xcshareddata/xcschemes/FlowCrypt.xcscheme | 2 +- .../xcshareddata/xcschemes/FlowCryptAppTests.xcscheme | 2 +- .../xcshareddata/xcschemes/FlowCryptCommon.xcscheme | 2 +- .../xcshareddata/xcschemes/FlowCryptUI.xcscheme | 2 +- .../xcshareddata/xcschemes/FlowCryptUIApplication.xcscheme | 2 +- Gemfile.lock | 1 + appium/README.md | 2 +- appium/tests/screenobjects/splash.screen.ts | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index e9f4ab826..337781cbd 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -2223,7 +2223,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1240; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1310; ORGANIZATIONNAME = "FlowCrypt Limited"; TargetAttributes = { 9F2AC5C5267BE99E00F6149B = { @@ -2577,7 +2577,6 @@ 21489B80267CC39E00BDE4AC /* ClientConfigurationService.swift in Sources */, D28655932423B4EE0066F52E /* MyMenuViewDecorator.swift in Sources */, 04B4728D1ECE29D200B8266F /* KeyInfoRealmObject.swift in Sources */, - 04B4728D1ECE29D200B8266F /* KeyInfoRealmObject.swift in Sources */, 9F3EF32F23B172D300FA0CEF /* SearchViewController.swift in Sources */, F191F621272511790053833E /* BlurViewController.swift in Sources */, D2F6D147243506DA00DB4065 /* MailSettingsCredentials.swift in Sources */, diff --git a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme index 8ceb7630c..688d5b1eb 100644 --- a/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme +++ b/FlowCrypt.xcodeproj/xcshareddata/xcschemes/Debug FlowCrypt.xcscheme @@ -1,6 +1,6 @@ > ~/.bash_profile` 5. restart terminal -6. `nvm install 12` - installs NodeJS 12 and sets it as default +6. `nvm install 16` - installs NodeJS 16 and sets it as default 7. `cd ~/git/flowcrypt-ios/appium && npm install` ## Run tests diff --git a/appium/tests/screenobjects/splash.screen.ts b/appium/tests/screenobjects/splash.screen.ts index b474b2a37..0bafba3fb 100644 --- a/appium/tests/screenobjects/splash.screen.ts +++ b/appium/tests/screenobjects/splash.screen.ts @@ -136,7 +136,7 @@ class SplashScreen extends BaseScreen { await browser.pause(1000); // stability sleep for language change if (await (await $(emailSelector)).isDisplayed()) { await ElementHelper.waitAndClick(await $(emailSelector)); - await (await this.useAnotherAcoount).waitForDisplayed({ timeout: 1000, reverse: true }); + await (await this.useAnotherAcoount).waitForDisplayed({ timeout: 5000, reverse: true }); if (await (await this.passwordField).isDisplayed()) { await this.fillPassword(password); await this.clickNextBtn(); From 4ac67a739497a5ffd4bae275e1a0416b3d8e615c Mon Sep 17 00:00:00 2001 From: tomholub Date: Tue, 23 Nov 2021 14:07:05 +0100 Subject: [PATCH 14/15] delays --- appium/tests/helpers/ElementHelper.ts | 2 +- appium/tests/screenobjects/contacts.screen.ts | 2 +- appium/tests/screenobjects/inbox.screen.ts | 4 +++- appium/tests/screenobjects/menu-bar.screen.ts | 8 ++++---- .../specs/composeEmail/SelectRecipientByName.spec.ts | 9 +++++++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/appium/tests/helpers/ElementHelper.ts b/appium/tests/helpers/ElementHelper.ts index 7071c4b12..1c53ad0ac 100644 --- a/appium/tests/helpers/ElementHelper.ts +++ b/appium/tests/helpers/ElementHelper.ts @@ -44,7 +44,7 @@ class ElementHelper { await element.doubleClick(); } - static waitAndClick = async (element: WebdriverIO.Element, delayMs = 10) => { + static waitAndClick = async (element: WebdriverIO.Element, delayMs = 50) => { await element.waitForDisplayed(); // stability fix to make sure element is ready for interaction await browser.pause(delayMs) diff --git a/appium/tests/screenobjects/contacts.screen.ts b/appium/tests/screenobjects/contacts.screen.ts index 81433621e..9c1be6962 100644 --- a/appium/tests/screenobjects/contacts.screen.ts +++ b/appium/tests/screenobjects/contacts.screen.ts @@ -38,7 +38,7 @@ class ContactsScreen extends BaseScreen { } clickBackButton = async () => { - await ElementHelper.waitAndClick(await this.backButton); + await ElementHelper.waitAndClick(await this.backButton, 1000); } checkContact = async (name: string) => { diff --git a/appium/tests/screenobjects/inbox.screen.ts b/appium/tests/screenobjects/inbox.screen.ts index 30df19907..127ed8779 100644 --- a/appium/tests/screenobjects/inbox.screen.ts +++ b/appium/tests/screenobjects/inbox.screen.ts @@ -1,5 +1,6 @@ import BaseScreen from './base.screen'; import ElementHelper from "../helpers/ElementHelper"; +import TouchHelper from "../helpers/TouchHelper" const SELECTORS = { ENTER_YOUR_PASS_PHRASE_FIELD: '-ios class chain:**/XCUIElementTypeSecureTextField[`value == "Enter your pass phrase"`]', @@ -19,7 +20,7 @@ class InboxScreen extends BaseScreen { } get inboxHeader() { - return $(SELECTORS.INBOX_HEADER) + return $(SELECTORS.INBOX_HEADER) } clickOnUserEmail = async (email: string) => { @@ -28,6 +29,7 @@ class InboxScreen extends BaseScreen { } clickOnEmailBySubject = async (subject: string) => { + await TouchHelper.scrollDown(); // todo - fix this await ElementHelper.waitAndClick(await $(`~${subject}`), 500); } diff --git a/appium/tests/screenobjects/menu-bar.screen.ts b/appium/tests/screenobjects/menu-bar.screen.ts index 61d93745f..d97d8b531 100644 --- a/appium/tests/screenobjects/menu-bar.screen.ts +++ b/appium/tests/screenobjects/menu-bar.screen.ts @@ -37,7 +37,7 @@ class MenuBarScreen extends BaseScreen { } get trashButton() { - return $(SELECTORS.TRASH_BTN) + return $(SELECTORS.TRASH_BTN) } clickMenuIcon = async () => { @@ -63,15 +63,15 @@ class MenuBarScreen extends BaseScreen { } clickInboxButton = async () => { - await ElementHelper.waitAndClick(await this.inboxButton); + await ElementHelper.waitAndClick(await this.inboxButton, 500); // todo - instead wait until loader gone } clickSentButton = async () => { - await ElementHelper.waitAndClick(await this.sentButton); + await ElementHelper.waitAndClick(await this.sentButton, 500); // todo - instead wait until loader gone } clickTrashButton = async () => { - await ElementHelper.waitAndClick(await this.trashButton); + await ElementHelper.waitAndClick(await this.trashButton, 500); // todo - instead wait until loader gone } } diff --git a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts index 540d33b98..40819fd99 100644 --- a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts +++ b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts @@ -43,15 +43,20 @@ describe('COMPOSE EMAIL: ', () => { await MenuBarScreen.clickInboxButton(); // Add first contact + await browser.pause(1000); await InboxScreen.clickCreateEmail(); - await NewMessageScreen.setAddRecipientByName(firstContactName, firstContactEmail); await NewMessageScreen.checkAddedRecipient(firstContactEmail); await NewMessageScreen.clickBackButton(); // Add second contact + await browser.pause(1000); // tom - else had issues on M1. Should add accessibility identifier just in case + // [iPhone 13 iOS 15.0 #0-1] Error: element ("-ios class chain:**/XCUIElementTypeButton[`label == "+"`]") still not displayed after 15000ms + // [iPhone 13 iOS 15.0 #0-1] at async Function.ElementHelper.waitAndClick (/Users/tom/git/flowcrypt-ios/appium/tests/helpers/ElementHelper.ts:48:5) + // [iPhone 13 iOS 15.0 #0-1] at async InboxScreen.clickCreateEmail (/Users/tom/git/flowcrypt-ios/appium/tests/screenobjects/inbox.screen.ts:37:5) + // [iPhone 13 iOS 15.0 #0-1] at async UserContext. (/Users/tom/git/flowcrypt-ios/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts:53:5) + // the element was in fact visible in the simulator when it crashed await InboxScreen.clickCreateEmail(); - await NewMessageScreen.setAddRecipientByName(secondContactName, secondContactEmail); await NewMessageScreen.checkAddedRecipient(secondContactEmail); await NewMessageScreen.clickBackButton(); From 68a4c42a0d0b605caac6bc330c014469811688cd Mon Sep 17 00:00:00 2001 From: tomholub Date: Tue, 23 Nov 2021 15:53:13 +0100 Subject: [PATCH 15/15] rm unneeded fix --- .../tests/specs/composeEmail/SelectRecipientByName.spec.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts index b8c94d620..1465e0dd1 100644 --- a/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts +++ b/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts @@ -44,19 +44,12 @@ describe('COMPOSE EMAIL: ', () => { await InboxScreen.checkInboxScreen(); // Add first contact - await browser.pause(1000); await InboxScreen.clickCreateEmail(); await NewMessageScreen.setAddRecipientByName(firstContactName, firstContactEmail); await NewMessageScreen.checkAddedRecipient(firstContactEmail); await NewMessageScreen.clickBackButton(); // Add second contact - await browser.pause(1000); // tom - else had issues on M1. Should add accessibility identifier just in case - // [iPhone 13 iOS 15.0 #0-1] Error: element ("-ios class chain:**/XCUIElementTypeButton[`label == "+"`]") still not displayed after 15000ms - // [iPhone 13 iOS 15.0 #0-1] at async Function.ElementHelper.waitAndClick (/Users/tom/git/flowcrypt-ios/appium/tests/helpers/ElementHelper.ts:48:5) - // [iPhone 13 iOS 15.0 #0-1] at async InboxScreen.clickCreateEmail (/Users/tom/git/flowcrypt-ios/appium/tests/screenobjects/inbox.screen.ts:37:5) - // [iPhone 13 iOS 15.0 #0-1] at async UserContext. (/Users/tom/git/flowcrypt-ios/appium/tests/specs/composeEmail/SelectRecipientByName.spec.ts:53:5) - // the element was in fact visible in the simulator when it crashed await InboxScreen.clickCreateEmail(); await NewMessageScreen.setAddRecipientByName(secondContactName, secondContactEmail); await NewMessageScreen.checkAddedRecipient(secondContactEmail);