From 41165e907d308b2e5c4515fbb0da82eba4ad59f5 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Sat, 18 Jan 2025 11:24:00 +0100 Subject: [PATCH 1/4] Improve Hub error handling and updates --- .../UnlockSectionFooterViewModel.swift | 13 +++++++----- .../VaultDetail/VaultDetailViewModel.swift | 4 ++-- .../Hub/CryptomatorHubAuthenticator.swift | 9 ++++++++- .../Hub/HubAuthenticationView.swift | 4 +++- .../Hub/HubAuthenticationViewModel.swift | 3 +++ .../HubDeviceRegisteredSuccessfullyView.swift | 19 ------------------ .../Hub/HubErrorWithRefreshView.swift | 20 +++++++++++++++++++ SharedResources/en.lproj/Localizable.strings | 5 +++-- 8 files changed, 47 insertions(+), 30 deletions(-) delete mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift diff --git a/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift b/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift index db95fee78..891c222f7 100644 --- a/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift +++ b/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift @@ -6,6 +6,7 @@ // Copyright © 2021 Skymatic GmbH. All rights reserved. // +import CryptomatorCloudAccessCore import CryptomatorCommonCore import Foundation @@ -31,21 +32,23 @@ class UnlockSectionFooterViewModel: HeaderFooterViewModel { } var biometryTypeName: String? + var vaultInfo: VaultInfo - init(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration) { + init(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration, vaultInfo: VaultInfo) { self.vaultUnlocked = vaultUnlocked self.biometricalUnlockEnabled = biometricalUnlockEnabled self.biometryTypeName = biometryTypeName - let titleText = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration) + let titleText = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration, vaultInfo: vaultInfo) self.title = Bindable(titleText) self.keepUnlockedDuration = keepUnlockedDuration + self.vaultInfo = vaultInfo } private func updateTitle() { - title.value = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration) + title.value = UnlockSectionFooterViewModel.getTitleText(vaultUnlocked: vaultUnlocked, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: biometryTypeName, keepUnlockedDuration: keepUnlockedDuration, vaultInfo: vaultInfo) } - private static func getTitleText(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration) -> String { + private static func getTitleText(vaultUnlocked: Bool, biometricalUnlockEnabled: Bool, biometryTypeName: String?, keepUnlockedDuration: KeepUnlockedDuration, vaultInfo: VaultInfo) -> String { let unlockedText: String if vaultUnlocked { unlockedText = LocalizedString.getValue("vaultDetail.unlocked.footer") @@ -62,7 +65,7 @@ class UnlockSectionFooterViewModel: HeaderFooterViewModel { keepUnlockedText = String(format: LocalizedString.getValue("vaultDetail.keepUnlocked.footer.limitedDuration"), keepUnlockedDuration.description ?? "") } var footerText = "\(unlockedText)\n\n\(keepUnlockedText)" - if let biometryTypeName = biometryTypeName { + if vaultInfo.vaultConfigType != .hub, let biometryTypeName = biometryTypeName { let biometricalUnlockText: String if biometricalUnlockEnabled { biometricalUnlockText = String(format: LocalizedString.getValue("vaultDetail.enabledBiometricalUnlock.footer"), biometryTypeName) diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index 0bc2872ae..934bed009 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -121,7 +121,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private var lockSectionCells: [BindableTableViewCellViewModel] { var cells: [BindableTableViewCellViewModel] = [lockButton, keepUnlockedCellViewModel] - if let biometryTypeName = context.enrolledBiometricsAuthenticationName() { + if vaultInfo.vaultConfigType != .hub, let biometryTypeName = context.enrolledBiometricsAuthenticationName() { let switchCellViewModel = getSwitchCellViewModel(biometryTypeName: biometryTypeName) cells.append(switchCellViewModel) } @@ -146,7 +146,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { .lockingSection: unlockSectionFooterViewModel, .removeVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.removeVault.footer"))] - private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value) + private lazy var unlockSectionFooterViewModel = UnlockSectionFooterViewModel(vaultUnlocked: vaultInfo.vaultIsUnlocked.value, biometricalUnlockEnabled: biometricalUnlockEnabled, biometryTypeName: context.enrolledBiometricsAuthenticationName(), keepUnlockedDuration: currentKeepUnlockedDuration.value, vaultInfo: vaultInfo) private lazy var vaultInfoCellViewModel = BindableTableViewCellViewModel(title: vaultInfo.vaultName, detailTitle: vaultInfo.vaultPath.path, detailTitleTextColor: .secondaryLabel, image: UIImage(vaultIconFor: vaultInfo.cloudProviderType, state: .normal), selectionStyle: .none) private lazy var renameVaultCellViewModel = ButtonCellViewModel.createDisclosureButton(action: VaultDetailButtonAction.showRenameVault, title: LocalizedString.getValue("vaultDetail.button.renameVault"), detailTitle: vaultName) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 7347d8843..853ed1043 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -20,6 +20,7 @@ public enum HubAuthenticationFlow { case needsDeviceRegistration case licenseExceeded case requiresAccountInitialization(at: URL) + case vaultArchived } public struct HubAuthenticationFlowSuccess { @@ -79,6 +80,8 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .requiresAccountInitialization(at: profileURL) case .legacyHubVersion: throw CryptomatorHubAuthenticatorError.incompatibleHubVersion + case .vaultArchived: + return .vaultArchived } let retrieveUserPrivateKeyResponse = try await getUserKey(apiBaseURL: apiBaseURL, authState: authState) @@ -240,8 +243,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(encryptedVaultKey: body, header: httpResponse?.allHeaderFields ?? [:]) case 402: return .licenseExceeded - case 403, 410: + case 403: return .accessNotGranted + case 410: + return .vaultArchived case 404: return .legacyHubVersion case 449: @@ -305,6 +310,8 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving case requiresAccountInitialization(at: URL) // 404 case legacyHubVersion + // 410 + case vaultArchived } private struct DeviceDto: Codable { diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift index 77e3c2815..df5b18340 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationView.swift @@ -20,7 +20,9 @@ public struct HubAuthenticationView: View { onRegisterTap: { Task { await viewModel.register() }} ) case .accessNotGranted: - HubAccessNotGrantedView(onRefresh: { Task { await viewModel.refresh() }}) + CryptomatorErrorWithRefreshView(headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted"), onRefresh: { Task { await viewModel.refresh() }}) + case .vaultArchived: + CryptomatorErrorWithRefreshView(headerTitle: LocalizedString.getValue("hubAuthentication.vaultArchived"), onRefresh: { Task { await viewModel.refresh() }}) case .licenseExceeded: CryptomatorErrorView(text: LocalizedString.getValue("hubAuthentication.licenseExceeded")) case let .error(description): diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift index ee81e7354..98ef71ae6 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubAuthenticationViewModel.swift @@ -32,6 +32,7 @@ public final class HubAuthenticationViewModel: ObservableObject { case licenseExceeded case deviceRegistration(DeviceRegistration) case error(description: String) + case vaultArchived } public enum DeviceRegistration: Equatable { @@ -108,6 +109,8 @@ public final class HubAuthenticationViewModel: ObservableObject { await setState(to: .licenseExceeded) case let .requiresAccountInitialization(profileURL): await delegate?.hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: profileURL) + case .vaultArchived: + await setState(to: .vaultArchived) } } diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift deleted file mode 100644 index c0328584f..000000000 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubDeviceRegisteredSuccessfullyView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct HubAccessNotGrantedView: View { - var onRefresh: () -> Void - - var body: some View { - CryptomatorSimpleButtonView( - buttonTitle: LocalizedString.getValue("common.button.refresh"), - onButtonTap: onRefresh, - headerTitle: LocalizedString.getValue("hubAuthentication.accessNotGranted") - ) - } -} - -struct HubDeviceRegisteredSuccessfullyView_Previews: PreviewProvider { - static var previews: some View { - HubAccessNotGrantedView(onRefresh: {}) - } -} diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift new file mode 100644 index 000000000..f8b3bd0a6 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/HubErrorWithRefreshView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct CryptomatorErrorWithRefreshView: View { + var headerTitle: String + var onRefresh: () -> Void + + var body: some View { + CryptomatorSimpleButtonView( + buttonTitle: LocalizedString.getValue("common.button.refresh"), + onButtonTap: onRefresh, + headerTitle: headerTitle + ) + } +} + +struct CryptomatorErrorWithRefreshView_Previews: PreviewProvider { + static var previews: some View { + CryptomatorErrorWithRefreshView(headerTitle: "Example Header Title", onRefresh: {}) + } +} diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 6445a8fd8..653debb8d 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -116,16 +116,17 @@ "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; "hubAuthentication.title" = "Hub Vault"; -"hubAuthentication.accessNotGranted" = "Your device has not yet been authorized to access this vault. Ask the vault owner to authorize it."; +"hubAuthentication.accessNotGranted" = "Your user has not yet been authorized to access this vault. Ask the vault owner to authorize it."; "hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; "hubAuthentication.deviceRegistration.accountKey.footer.title" = "Your Account Key is required to login from new apps or browsers. It can be found in your profile."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; -"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault, your device needs to be authorized by the vault owner."; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault"; "hubAuthentication.requireAccountInit.alert.title" = "Action Required"; "hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; "hubAuthentication.requireAccountInit.alert.actionButton" = "Go to Profile"; +"hubAuthentication.vaultArchived" = "This vault has been archived. Please ask the vault owner to unarchive it."; "intents.saveFile.missingFile" = "The provided file is not valid."; "intents.saveFile.invalidFolder" = "The provided folder is not valid."; From 77e23387975c4f04318178606bff9ce6e92f24aa Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 20 Jan 2025 09:31:26 +0100 Subject: [PATCH 2/4] Adjust localized strings and resolve SwiftLint violations --- .../Hub/CryptomatorHubAuthenticator.swift | 3 +++ SharedResources/en.lproj/Localizable.strings | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 853ed1043..52f1a77d4 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -49,6 +49,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public init() {} + // swiftlint:disable cyclomatic_complexity public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { guard let hubConfig = vaultConfig.allegedHubConfig, let vaultBaseURL = getVaultBaseURL(from: vaultConfig) else { throw CryptomatorHubAuthenticatorError.invalidVaultConfig @@ -100,6 +101,8 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: unlockHeader)) } + // swiftlint:enable cyclomatic_complexity + /** Registers a new device. diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 653debb8d..be744841c 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -116,13 +116,13 @@ "getFolderIntent.error.noVaultSelected" = "No vault has been selected."; "hubAuthentication.title" = "Hub Vault"; -"hubAuthentication.accessNotGranted" = "Your user has not yet been authorized to access this vault. Ask the vault owner to authorize it."; +"hubAuthentication.accessNotGranted" = "You do not have permission to access this vault. Ask the vault owner to authorize you."; "hubAuthentication.licenseExceeded" = "Your Cryptomator Hub instance has an invalid license. Please inform a Hub administrator to upgrade or renew the license."; "hubAuthentication.deviceRegistration.deviceName.cells.name" = "Device Name"; "hubAuthentication.deviceRegistration.deviceName.footer.title" = "This seems to be the first Hub access from this device. In order to identify it for access authorization, you need to name this device."; "hubAuthentication.deviceRegistration.accountKey.footer.title" = "Your Account Key is required to login from new apps or browsers. It can be found in your profile."; "hubAuthentication.deviceRegistration.needsAuthorization.alert.title" = "Register Device Successful"; -"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault"; +"hubAuthentication.deviceRegistration.needsAuthorization.alert.message" = "To access the vault, the vault owner needs to grant you permission."; "hubAuthentication.requireAccountInit.alert.title" = "Action Required"; "hubAuthentication.requireAccountInit.alert.message" = "To proceed, please complete the steps required in your Hub user profile."; "hubAuthentication.requireAccountInit.alert.actionButton" = "Go to Profile"; From 1bb1c634f456325e9d45a6201e223cf18ab57409 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 20 Jan 2025 10:04:16 +0100 Subject: [PATCH 3/4] Fix inconsistency in HTTP 410 status handling --- .../CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 52f1a77d4..4f4cca447 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -305,7 +305,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving private enum RetrieveVaultMasterkeyEncryptedForUserResponse { // 200 case success(encryptedVaultKey: String, header: [AnyHashable: Any]) - // 403, 410 + // 403 case accessNotGranted // 402 case licenseExceeded From fb5795cc6bbabae02eacec06d33dc6e976261245 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Thu, 6 Feb 2025 08:56:58 +0100 Subject: [PATCH 4/4] Simplified disabling SwiftLint --- .../Hub/CryptomatorHubAuthenticator.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 4f4cca447..766f03ae3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -49,7 +49,7 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving public init() {} - // swiftlint:disable cyclomatic_complexity + // swiftlint:disable:next cyclomatic_complexity public func receiveKey(authState: OIDAuthState, vaultConfig: UnverifiedVaultConfig) async throws -> HubAuthenticationFlow { guard let hubConfig = vaultConfig.allegedHubConfig, let vaultBaseURL = getVaultBaseURL(from: vaultConfig) else { throw CryptomatorHubAuthenticatorError.invalidVaultConfig @@ -101,8 +101,6 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving return .success(.init(encryptedUserKey: encryptedUserKeyJWE, encryptedVaultKey: encryptedVaultKeyJWE, header: unlockHeader)) } - // swiftlint:enable cyclomatic_complexity - /** Registers a new device.