From 483223f23b1f41e2e385e54d4b946c8248d1b3b0 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 15 Oct 2024 12:16:51 +0200 Subject: [PATCH] Add UnauthorizedError screen for invalid cloud access in Files app (#384) --- Cryptomator.xcodeproj/project.pbxproj | 8 ++ .../FileProviderCoordinator.swift | 18 ++++- .../FileProviderCoordinatorError.swift | 13 +++ .../UnauthorizedErrorViewController.swift | 81 +++++++++++++++++++ SharedResources/en.lproj/Localizable.strings | 1 + 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 FileProviderExtensionUI/FileProviderCoordinatorError.swift create mode 100644 FileProviderExtensionUI/UnauthorizedErrorViewController.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 69493c602..7dfbf7e15 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -434,6 +434,8 @@ 74F5DC1C26DCD2FB00AFE989 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */; }; 74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; }; 74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; }; + B330CB452CB5735300C21E03 /* UnauthorizedErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */; }; + B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1041,6 +1043,8 @@ 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = ""; }; 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; + B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = ""; }; + B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1663,7 +1667,9 @@ 4A9FCB0B251A02A3002A8B41 /* FileProviderExtensionUI.entitlements */, 4AA621EB249A6A8400A0BCBD /* Info.plist */, 4A6A521A268B7147006F7368 /* FileProviderCoordinator.swift */, + B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */, 4A6A5218268B6D31006F7368 /* OnboardingViewController.swift */, + B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */, 4A6A520C268B5EF7006F7368 /* RootViewController.swift */, 4A9BED65268F2D9C00721BAA /* UnlockVaultViewController.swift */, 4AFD8C0E269304A700F77BA6 /* UnlockVaultViewModel.swift */, @@ -2670,6 +2676,8 @@ 4A804082276952C300D7D999 /* FileProviderCoordinatorSnapshotMock.swift in Sources */, 4A9BED66268F2D9D00721BAA /* UnlockVaultViewController.swift in Sources */, 4A6A521B268B7147006F7368 /* FileProviderCoordinator.swift in Sources */, + B330CB452CB5735300C21E03 /* UnauthorizedErrorViewController.swift in Sources */, + B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */, 4A6A5219268B6D32006F7368 /* OnboardingViewController.swift in Sources */, 4AFD8C0F269304A700F77BA6 /* UnlockVaultViewModel.swift in Sources */, ); diff --git a/FileProviderExtensionUI/FileProviderCoordinator.swift b/FileProviderExtensionUI/FileProviderCoordinator.swift index 40bc0ff09..eb74e39f5 100644 --- a/FileProviderExtensionUI/FileProviderCoordinator.swift +++ b/FileProviderExtensionUI/FileProviderCoordinator.swift @@ -63,9 +63,13 @@ class FileProviderCoordinator: Coordinator { func handleError(_ error: Error, for viewController: UIViewController) { DDLogError("Error: \(error)") - let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: error.localizedDescription, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default)) - viewController.present(alertController, animated: true) + if let fileProviderError = error as? FileProviderCoordinatorError, case let .unauthorized(vaultName) = fileProviderError { + showUnauthorizedError(vaultName: vaultName) + } else { + let alertController = UIAlertController(title: LocalizedString.getValue("common.alert.error.title"), message: error.localizedDescription, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default)) + viewController.present(alertController, animated: true) + } } func done() { @@ -80,6 +84,12 @@ class FileProviderCoordinator: Coordinator { navigationController.pushViewController(onboardingVC, animated: false) } + func showUnauthorizedError(vaultName: String) { + let unauthorizedErrorVC = UnauthorizedErrorViewController(vaultName: vaultName) + unauthorizedErrorVC.coordinator = self + navigationController.pushViewController(unauthorizedErrorVC, animated: true) + } + func openCryptomatorApp() { let url = URL(string: "cryptomator:")! extensionContext.open(url) { success in @@ -134,6 +144,8 @@ class FileProviderCoordinator: Coordinator { switch error { case CloudProviderError.noInternetConnection, LocalizedCloudProviderError.itemNotFound: break + case LocalizedCloudProviderError.unauthorized: + throw FileProviderCoordinatorError.unauthorized(vaultName: domain.displayName) default: throw error } diff --git a/FileProviderExtensionUI/FileProviderCoordinatorError.swift b/FileProviderExtensionUI/FileProviderCoordinatorError.swift new file mode 100644 index 000000000..9c097d2d0 --- /dev/null +++ b/FileProviderExtensionUI/FileProviderCoordinatorError.swift @@ -0,0 +1,13 @@ +// +// FileProviderCoordinatorError.swift +// Cryptomator +// +// Created by Majid Achhoud on 11.10.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Foundation + +public enum FileProviderCoordinatorError: Error { + case unauthorized(vaultName: String) +} diff --git a/FileProviderExtensionUI/UnauthorizedErrorViewController.swift b/FileProviderExtensionUI/UnauthorizedErrorViewController.swift new file mode 100644 index 000000000..10797c1dd --- /dev/null +++ b/FileProviderExtensionUI/UnauthorizedErrorViewController.swift @@ -0,0 +1,81 @@ +// +// UnauthorizedErrorViewController.swift +// Cryptomator +// +// Created by Majid Achhoud on 08.10.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import UIKit + +class UnauthorizedErrorViewController: UITableViewController { + weak var coordinator: FileProviderCoordinator? + private var vaultName: String + + private lazy var openCryptomatorCell: UITableViewCell = { + let cell = UITableViewCell() + cell.textLabel?.text = LocalizedString.getValue("fileProvider.onboarding.button.openCryptomator") + cell.textLabel?.textColor = .cryptomatorPrimary + return cell + }() + + init(vaultName: String) { + self.vaultName = vaultName + super.init(style: .insetGrouped) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = vaultName + let doneButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(done)) + navigationItem.rightBarButtonItem = doneButton + tableView.backgroundColor = .cryptomatorBackground + tableView.cellLayoutMarginsFollowReadableWidth = true + } + + @objc func done() { + coordinator?.userCancelled() + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return openCryptomatorCell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + return UnauthorizedErrorHeaderView(vaultName: vaultName) + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + coordinator?.openCryptomatorApp() + } +} + +private class UnauthorizedErrorHeaderView: LargeHeaderFooterView { + init(vaultName: String) { + let config = UIImage.SymbolConfiguration(pointSize: 120) + let symbolImage = UIImage(systemName: "exclamationmark.triangle.fill", withConfiguration: config)?.withTintColor(.systemYellow, renderingMode: .alwaysOriginal) + + let infoText = String(format: LocalizedString.getValue("fileprovider.error.unauthorized.text"), vaultName) + + super.init(image: symbolImage, infoText: infoText) + } +} diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 5a1eb77f1..6445a8fd8 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -101,6 +101,7 @@ "fileProvider.error.biometricalAuthWrongPassword.message" = "The password that has been saved for %@ is wrong. Please try again and enter your password to re-enable %@."; "fileProvider.error.defaultLock.title" = "Unlock Required"; "fileProvider.error.defaultLock.message" = "To access and show the contents of your vault, it has to be unlocked."; +"fileprovider.error.unauthorized.text" = "Access to your vault \"%@\" was denied. Open the main app to check your connection and re-authenticate if needed."; "fileProvider.error.unlockButton" = "Unlock"; "fileProvider.clearFileFromCache.title" = "Clear File from Cache"; "fileProvider.clearFileFromCache.message" = "This only removes the local file from your device and does not delete the file in the cloud.";