From d76c000016f99b5a7ca3037d00361bf50ae5b317 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 23 Dec 2024 20:31:20 +0100 Subject: [PATCH 1/7] WIP - Create new EnterURLScreen for SharePoint, add SharePoint Icons and add SharePoint as CloudProvider --- Cryptomator.xcodeproj/project.pbxproj | 17 +++++ .../CreateNewVaultCoordinator.swift | 32 ++++++-- .../EnterSharePointURLViewController.swift | 41 ++++++++++ .../EnterSharePointURLViewModel.swift | 72 ++++++++++++++++++ .../CreateNewVault/SharePointURLSetting.swift | 14 ++++ .../CreateNewVault/URLValidator.swift | 34 +++++++++ .../OpenExistingVaultCoordinator.swift | 2 +- Cryptomator/AppDelegate.swift | 10 +-- .../AccountListViewController.swift | 2 +- .../AccountListViewModel.swift | 8 ++ Cryptomator/Common/CloudAuthenticator.swift | 19 ++++- .../CloudProviderType+Localization.swift | 2 + .../Common/UIImage+CloudProviderType.swift | 4 + .../Settings/SettingsCoordinator.swift | 2 +- .../VaultDetailInfoFooterViewModel.swift | 3 + .../Manager/CloudProviderDBManager.swift | 6 ++ .../Manager/CloudProviderType.swift | 1 + .../SaveFileIntentHandler.swift | 2 + .../FileProviderExtension.swift | 8 +- .../RootViewController.swift | 8 +- .../Contents.json | 23 ++++++ .../sharepoint-vault-selected.png | Bin 0 -> 1315 bytes .../sharepoint-vault-selected@2x.png | Bin 0 -> 3091 bytes .../sharepoint-vault-selected@3x.png | Bin 0 -> 2794 bytes .../sharepoint-vault.imageset/Contents.json | 23 ++++++ .../sharepoint-vault.png | Bin 0 -> 1645 bytes .../sharepoint-vault@2x.png | Bin 0 -> 3969 bytes .../sharepoint-vault@3x.png | Bin 0 -> 3418 bytes .../sharepoint.imageset/Contents.json | 23 ++++++ .../sharepoint.imageset/sharepoint.png | Bin 0 -> 1083 bytes .../sharepoint.imageset/sharepoint@2x.png | Bin 0 -> 2559 bytes .../sharepoint.imageset/sharepoint@3x.png | Bin 0 -> 3480 bytes SharedResources/en.lproj/Localizable.strings | 7 ++ 33 files changed, 338 insertions(+), 25 deletions(-) create mode 100644 Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift create mode 100644 Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift create mode 100644 Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift create mode 100644 Cryptomator/AddVault/CreateNewVault/URLValidator.swift create mode 100644 SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json create mode 100644 SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png create mode 100644 SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png create mode 100644 SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png create mode 100644 SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json create mode 100644 SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png create mode 100644 SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@2x.png create mode 100644 SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png create mode 100644 SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json create mode 100644 SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png create mode 100644 SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@2x.png create mode 100644 SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint@3x.png diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index e9cfa0612..800a64997 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -433,6 +433,10 @@ 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 */; }; + B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */; }; + B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */; }; + B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */; }; + B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C532B2D142BE000F30FE9 /* URLValidator.swift */; }; B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; }; /* End PBXBuildFile section */ @@ -1040,6 +1044,11 @@ 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 = ""; }; + B34C53212D1355D900F30FE9 /* cloud-access-swift */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "cloud-access-swift"; path = "../cloud-access-swift"; sourceTree = SOURCE_ROOT; }; + B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = ""; }; + B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = ""; }; + B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointURLSetting.swift; sourceTree = ""; }; + B34C532B2D142BE000F30FE9 /* URLValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidator.swift; sourceTree = ""; }; B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1378,6 +1387,10 @@ 4A644B45267A3D21008CBB9A /* CreateNewVault */ = { isa = PBXGroup; children = ( + B34C532B2D142BE000F30FE9 /* URLValidator.swift */, + B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */, + B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */, + B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */, 4A644B4A267B4C08008CBB9A /* CreateNewVaultChooseFolderViewController.swift */, 4A53CC16267CDBFF00853BB3 /* CreateNewVaultChooseFolderViewModel.swift */, 4A644B4C267B55E4008CBB9A /* CreateNewVaultCoordinator.swift */, @@ -2718,6 +2731,7 @@ 4A6A521D268B7C8F006F7368 /* BaseNavigationController.swift in Sources */, 4AC005F127C3D80B006FFE87 /* PremiumManager.swift in Sources */, 4ADD2342267383BE00374E4E /* AddVaultSuccessViewModel.swift in Sources */, + B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */, 4AB1D4F827D68026009060AB /* IAPHeaderView.swift in Sources */, 4A79E26926B16993008C9959 /* ActionButton.swift in Sources */, 4AF91CD925A722A600ACF01E /* VaultInfo.swift in Sources */, @@ -2820,17 +2834,20 @@ 4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */, 4A3D658226838991000DA764 /* OpenExistingLocalVaultViewModel.swift in Sources */, 4AC86270273598CC00E15BA5 /* UIViewController+ProgressHUDError.swift in Sources */, + B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */, 4AB1D4FD27D69BB2009060AB /* TrialCell.swift in Sources */, 4A2FD08225B5E2BA008565C8 /* VaultInstalling.swift in Sources */, 74C2BC5226E8FCD000BCAA03 /* PurchaseCoordinator.swift in Sources */, 4A53B6D32722F92D000DC367 /* MoveVaultViewModel.swift in Sources */, 4A4B7E4426B2B1A5009BFDB1 /* BindableTableViewCellViewModel.swift in Sources */, + B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */, 4AED9A69286B303000352951 /* S3Authenticator+VC.swift in Sources */, 4ADBD35827284BAB00B19B5C /* MoveVaultViewController.swift in Sources */, 7408E6CD26779BCC00D7FAEA /* AboutViewModel.swift in Sources */, 4A8A6424286CA72B001F5EB9 /* DefaultShowEditAccountBehavior.swift in Sources */, 4AB1D4FF27D69C9A009060AB /* DisclosureCell.swift in Sources */, 4A7077FF278DC2ED00AEF4CE /* VaultKeepUnlockedViewController.swift in Sources */, + B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */, 4A21B49C26BD68C2000D13DF /* UIControl+Publisher.swift in Sources */, 4AF91CD025A71C5800ACF01E /* UIImage+CloudProviderType.swift in Sources */, 4A4B7E4A26B2C071009BFDB1 /* ButtonCellViewModel.swift in Sources */, diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index 3f34fc168..2a425edc6 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -11,12 +11,13 @@ import CryptomatorCloudAccessCore import CryptomatorCommonCore import UIKit -class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { +class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator, SharePointURLSetting { var navigationController: UINavigationController var childCoordinators = [Coordinator]() weak var parentCoordinator: Coordinator? private let vaultName: String + private var currentSharePointAccount: AccountInfo? init(navigationController: UINavigationController, vaultName: String) { self.navigationController = navigationController @@ -24,7 +25,7 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA } func start() { - let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header")) + let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.createNewVault.chooseCloud.header")) let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("addVault.createNewVault.title") chooseCloudVC.coordinator = self @@ -44,15 +45,34 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared) - authenticator.authenticate(cloudProviderType, from: viewController).then { account in + authenticator.authenticate(cloudProviderType, from: viewController).then { _ in + } + } + + func showEnterSharePointURL(for account: AccountInfo) { + let viewModel = EnterSharePointURLViewModel(account: account) + let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel) + enterURLVC.coordinator = self + navigationController.pushViewController(enterURLVC, animated: true) + } + + func setSharePointURL(_ url: String) { + guard let account = currentSharePointAccount else { return } + do { let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - self.startFolderChooser(with: provider, account: account) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } catch { + handleError(error, for: navigationController) } } func selectedAccont(_ account: AccountInfo) throws { - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - startFolderChooser(with: provider, account: account.cloudProviderAccount) + if account.cloudProviderType == .sharePoint { + showEnterSharePointURL(for: account) + } else { + let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } } private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) { diff --git a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift new file mode 100644 index 000000000..2a8b359b8 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift @@ -0,0 +1,41 @@ +// +//  EnterSharePointURLViewController.swift +//  Cryptomator +// +//  Created by Majid Achhoud on 03.12.24. +// + +import Combine +import CryptomatorCommonCore +import UIKit + +class EnterSharePointURLViewController: SingleSectionStaticUITableViewController { + weak var coordinator: (SharePointURLSetting & Coordinator)? + private var viewModel: EnterSharePointURLViewModelProtocol + private var lastReturnButtonPressedSubscriber: AnyCancellable? + init(viewModel: EnterSharePointURLViewModelProtocol) { + self.viewModel = viewModel + super.init(viewModel: viewModel) + } + + override func viewDidLoad() { + super.viewDidLoad() + let doneButton = UIBarButtonItem(title: LocalizedString.getValue("common.button.next"), style: .done, target: self, action: #selector(nextButtonClicked)) + navigationItem.rightBarButtonItem = doneButton + lastReturnButtonPressedSubscriber = viewModel.lastReturnButtonPressed.sink { [weak self] in + self?.lastReturnButtonPressedAction() + } + } + + @objc func nextButtonClicked() { + do { + try coordinator?.setSharePointURL(viewModel.getValidatedSharePointURL()) + } catch { + coordinator?.handleError(error, for: self) + } + } + + func lastReturnButtonPressedAction() { + nextButtonClicked() + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift new file mode 100644 index 000000000..943668420 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewModel.swift @@ -0,0 +1,72 @@ +// +// EnterSharePointURLViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Combine +import CryptomatorCommonCore +import Foundation + +protocol EnterSharePointURLViewModelProtocol: SingleSectionTableViewModel, ReturnButtonSupport { + func getValidatedSharePointURL() throws -> String +} + +class EnterSharePointURLViewModel: SingleSectionTableViewModel, EnterSharePointURLViewModelProtocol { + let account: AccountInfo + init(account: AccountInfo) { + self.account = account + } + + var lastReturnButtonPressed: AnyPublisher { + return setupReturnButtonSupport(for: [sharePointURLCellViewModel], subscribers: &subscribers) + } + + override var cells: [TableViewCellViewModel] { + return [sharePointURLCellViewModel] + } + + override var title: String? { + return LocalizedString.getValue("addVault.enterSharePointURL.title") + } + + let sharePointURLCellViewModel = TextFieldCellViewModel( + type: .normal, + placeholder: LocalizedString.getValue("addVault.enterSharePointURL.placeholder"), + isInitialFirstResponder: true + ) + var trimmedSharePointURL: String { + return sharePointURLCellViewModel.input.value.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private lazy var subscribers = Set() + func getValidatedSharePointURL() throws -> String { + guard !trimmedSharePointURL.isEmpty else { + throw EnterSharePointURLViewModelError.emptyURL + } + try URLValidator.validateSharePointURL(urlString: trimmedSharePointURL) + return trimmedSharePointURL + } + + override func getHeaderTitle(for section: Int) -> String? { + guard section == 0 else { + return nil + } + return LocalizedString.getValue("addVault.enterSharePointURL.header.title") + } +} + +enum EnterSharePointURLViewModelError: LocalizedError { + case emptyURL + case invalidURL + var errorDescription: String? { + switch self { + case .emptyURL: + return LocalizedString.getValue("addVault.enterSharePointURL.error.emptyURL") + case .invalidURL: + return LocalizedString.getValue("addVault.enterSharePointURL.error.invalidURL") + } + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift b/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift new file mode 100644 index 000000000..2ee2e5a90 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointURLSetting.swift @@ -0,0 +1,14 @@ +// +// SharePointURLSetting.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import UIKit + +protocol SharePointURLSetting: AnyObject { + func setSharePointURL(_ url: String) +} diff --git a/Cryptomator/AddVault/CreateNewVault/URLValidator.swift b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift new file mode 100644 index 000000000..97cbd527d --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift @@ -0,0 +1,34 @@ +// +// URLValidator.swift +// Cryptomator +// +// Created by Majid Achhoud on 03.12.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import Foundation + +public enum URLValidatorError: Error, Equatable { + case invalidURLFormat +} + +extension URLValidatorError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidURLFormat: + return LocalizedString.getValue("addVault.enterSharePointURL.error.invalidURL") + } + } +} + +public enum URLValidator { + public static func validateSharePointURL(urlString: String) throws { + let pattern = #"^https:\/\/[a-zA-Z0-9\-]+\.sharepoint\.com\/sites\/[a-zA-Z0-9\-]+$"# + let regex = try NSRegularExpression(pattern: pattern) + let range = NSRange(location: 0, length: urlString.utf16.count) + if regex.firstMatch(in: urlString, options: [], range: range) == nil { + throw URLValidatorError.invalidURLFormat + } + } +} diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index cb5a01c17..f9d5f8756 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -25,7 +25,7 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } func start() { - let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.openExistingVault.chooseCloud.header")) + let viewModel = ChooseCloudViewModel(clouds: [.localFileSystem(type: .iCloudDrive), .dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom), .localFileSystem(type: .custom)], headerTitle: LocalizedString.getValue("addVault.openExistingVault.chooseCloud.header")) let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("addVault.openExistingVault.title") chooseCloudVC.coordinator = self diff --git a/Cryptomator/AppDelegate.swift b/Cryptomator/AppDelegate.swift index 578578501..f9a96a4b7 100644 --- a/Cryptomator/AppDelegate.swift +++ b/Cryptomator/AppDelegate.swift @@ -41,10 +41,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: nil) do { - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: nil) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: nil) } catch { DDLogError("Setting up OneDrive failed with error: \(error)") } @@ -87,7 +87,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } else if url.scheme == CloudAccessSecrets.googleDriveRedirectURLScheme { return GoogleDriveAuthenticator.currentAuthorizationFlow?.resumeExternalUserAgentFlow(with: url) ?? false - } else if url.scheme == CloudAccessSecrets.oneDriveRedirectURIScheme { + } else if url.scheme == CloudAccessSecrets.microsoftGraphRedirectURIScheme { return MSALPublicClientApplication.handleMSALResponse(url, sourceApplication: options[.sourceApplication] as? String) } return false diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift index f32d81180..31700c3e5 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewController.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewController.swift @@ -142,7 +142,7 @@ class AccountListViewController: ListViewController, ASWebAu private func supportsEditing(_ cloudProviderType: CloudProviderType) -> Bool { switch cloudProviderType { - case .box, .dropbox, .googleDrive, .localFileSystem, .oneDrive, .pCloud: + case .box, .dropbox, .googleDrive, .localFileSystem, .oneDrive, .sharePoint, .pCloud: return false case .s3, .webDAV: return true diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift index 79d62b8ff..a27867d50 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift @@ -89,6 +89,9 @@ class AccountListViewModel: AccountListViewModelProtocol { case .oneDrive: let credential = try OneDriveCredential(with: accountInfo.accountUID) return try createAccountCellContent(for: credential) + case .sharePoint: + let credential = try SharePointCredential(with: accountInfo.accountUID) + return try createAccountCellContent(for: credential) case .pCloud: return createAccountCellContentPlaceholder() case .s3: @@ -125,6 +128,11 @@ class AccountListViewModel: AccountListViewModelProtocol { return AccountCellContent(mainLabelText: username, detailLabelText: nil) } + func createAccountCellContent(for credential: SharePointCredential) throws -> AccountCellContent { + let username = try credential.getUsername() + return AccountCellContent(mainLabelText: username, detailLabelText: nil) + } + func createAccountCellContent(for credential: PCloudCredential) -> Promise { return credential.getUsername().then { username in AccountCellContent(mainLabelText: username, detailLabelText: nil) diff --git a/Cryptomator/Common/CloudAuthenticator.swift b/Cryptomator/Common/CloudAuthenticator.swift index 21258e0ef..07fdf53a7 100644 --- a/Cryptomator/Common/CloudAuthenticator.swift +++ b/Cryptomator/Common/CloudAuthenticator.swift @@ -43,14 +43,22 @@ class CloudAuthenticator { } } - func authenticateOneDrive(from viewController: UIViewController) -> Promise { - OneDriveAuthenticator.authenticate(from: viewController).then { credential -> CloudProviderAccount in - let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .oneDrive) + func authenticateMicrosoftGraph(from viewController: UIViewController, providerType: CloudProviderType) -> Promise { + return MicrosoftGraphAuthenticator.authenticate(from: viewController, for: providerType).then { credential -> CloudProviderAccount in + let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: providerType) try self.accountManager.saveNewAccount(account) return account } } + func authenticateOneDrive(from viewController: UIViewController) -> Promise { + return authenticateMicrosoftGraph(from: viewController, providerType: .oneDrive) + } + + func authenticateSharePoint(from viewController: UIViewController) -> Promise { + return authenticateMicrosoftGraph(from: viewController, providerType: .sharePoint) + } + func authenticatePCloud(from viewController: UIViewController) -> Promise { return PCloudAuthenticator.authenticate(from: viewController).then { credential -> CloudProviderAccount in try credential.saveToKeychain() @@ -103,6 +111,8 @@ class CloudAuthenticator { return authenticateOneDrive(from: viewController) case .pCloud: return authenticatePCloud(from: viewController) + case .sharePoint: + return authenticateSharePoint(from: viewController) case .s3: return authenticateS3(from: viewController) case .webDAV: @@ -127,6 +137,9 @@ class CloudAuthenticator { case .oneDrive: let credential = try OneDriveCredential(with: account.accountUID) try credential.deauthenticate() + case .sharePoint: + let credential = try SharePointCredential(with: account.accountUID) + try credential.deauthenticate() case .pCloud: let credential = try PCloudCredential(userID: account.accountUID) try credential.deauthenticate() diff --git a/Cryptomator/Common/CloudProviderType+Localization.swift b/Cryptomator/Common/CloudProviderType+Localization.swift index 79b2d8ba0..92aad6a3d 100644 --- a/Cryptomator/Common/CloudProviderType+Localization.swift +++ b/Cryptomator/Common/CloudProviderType+Localization.swift @@ -22,6 +22,8 @@ extension CloudProviderType { return localFileSystemType.localizedString() case .oneDrive: return "OneDrive" + case .sharePoint: + return "SharePoint" case .pCloud: return "pCloud" case .s3: diff --git a/Cryptomator/Common/UIImage+CloudProviderType.swift b/Cryptomator/Common/UIImage+CloudProviderType.swift index 4e2a4b09c..d8bb007cf 100644 --- a/Cryptomator/Common/UIImage+CloudProviderType.swift +++ b/Cryptomator/Common/UIImage+CloudProviderType.swift @@ -28,6 +28,8 @@ extension UIImage { assetName = UIImage.getVaultIcon(for: localFileSystemType) case .oneDrive: assetName = "onedrive-vault" + case .sharePoint: + assetName = "sharepoint-vault" case .pCloud: assetName = "pcloud-vault" case .s3: @@ -63,6 +65,8 @@ extension UIImage { assetName = UIImage.getStorageIcon(for: localFileSystemType) case .oneDrive: assetName = "onedrive" + case .sharePoint: + assetName = "sharepoint" case .pCloud: assetName = "pcloud" case .s3: diff --git a/Cryptomator/Settings/SettingsCoordinator.swift b/Cryptomator/Settings/SettingsCoordinator.swift index a2a3c5ba6..32e8705b3 100644 --- a/Cryptomator/Settings/SettingsCoordinator.swift +++ b/Cryptomator/Settings/SettingsCoordinator.swift @@ -49,7 +49,7 @@ class SettingsCoordinator: Coordinator { } func showCloudServices() { - let viewModel = ChooseCloudViewModel(clouds: [.dropbox, .googleDrive, .oneDrive, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "") + let viewModel = ChooseCloudViewModel(clouds: [.dropbox, .googleDrive, .oneDrive, .sharePoint, .pCloud, .box, .webDAV(type: .custom), .s3(type: .custom)], headerTitle: "") let chooseCloudVC = ChooseCloudViewController(viewModel: viewModel) chooseCloudVC.title = LocalizedString.getValue("settings.cloudServices") chooseCloudVC.coordinator = self diff --git a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift index b2b8b2fe1..d7052fd46 100644 --- a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift @@ -67,6 +67,9 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode } getUsername(for: credential) return "(…)" + case .sharePoint: + let credential = try? SharePointCredential(with: vault.delegateAccountUID) + return try? credential?.getUsername() case .s3: guard let displayName = try? S3CredentialManager.shared.getDisplayName(for: vault.delegateAccountUID) else { return nil diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 5a3d4fadf..932870fbf 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -80,6 +80,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .oneDrive: let credential = try OneDriveCredential(with: accountUID) provider = try OneDriveCloudProvider(credential: credential, maxPageSize: .max) + case .sharePoint: + let credential = try SharePointCredential(with: accountUID) + provider = try MicrosoftGraphCloudProvider(credential: credential, maxPageSize: .max) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createClient(with: credential.user) @@ -131,6 +134,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .oneDrive: let credential = try OneDriveCredential(with: accountUID) provider = try OneDriveCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) + case .sharePoint: + let credential = try SharePointCredential(with: accountUID) + provider = try OneDriveCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createBackgroundClient(with: credential.user, sessionIdentifier: sessionIdentifier) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift index 7ebdb1a5c..1c8fe6266 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderType.swift @@ -15,6 +15,7 @@ public enum CloudProviderType: Codable, Equatable, Hashable { case googleDrive case localFileSystem(type: LocalFileSystemType) case oneDrive + case sharePoint case pCloud case s3(type: S3Type) case webDAV(type: WebDAVType) diff --git a/CryptomatorIntents/SaveFileIntentHandler.swift b/CryptomatorIntents/SaveFileIntentHandler.swift index d2a8dc37d..eb9f90432 100644 --- a/CryptomatorIntents/SaveFileIntentHandler.swift +++ b/CryptomatorIntents/SaveFileIntentHandler.swift @@ -133,6 +133,8 @@ extension CloudProviderType { return type.assetName case .oneDrive: return "onedrive-vault" + case .sharePoint: + return "sharepoint-vault" case .pCloud: return "pcloud-vault" case .s3: diff --git a/FileProviderExtension/FileProviderExtension.swift b/FileProviderExtension/FileProviderExtension.swift index def01d6d2..484f97945 100644 --- a/FileProviderExtension/FileProviderExtension.swift +++ b/FileProviderExtension/FileProviderExtension.swift @@ -30,10 +30,10 @@ class FileProviderExtension: NSFileProviderExtension { FileProviderExtension.sharedDatabaseInitialized = true DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: false) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: CryptomatorConstants.appGroupName) - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: CryptomatorConstants.appGroupName) PCloudSetup.constants = PCloudSetup(appKey: CloudAccessSecrets.pCloudAppKey, sharedContainerIdentifier: CryptomatorConstants.appGroupName) BoxSetup.constants = BoxSetup(clientId: CloudAccessSecrets.boxClientId, clientSecret: CloudAccessSecrets.boxClientSecret, sharedContainerIdentifier: CryptomatorConstants.appGroupName) } catch { diff --git a/FileProviderExtensionUI/RootViewController.swift b/FileProviderExtensionUI/RootViewController.swift index 2df85a802..ca9301811 100644 --- a/FileProviderExtensionUI/RootViewController.swift +++ b/FileProviderExtensionUI/RootViewController.swift @@ -64,10 +64,10 @@ class RootViewController: FPUIActionExtensionViewController { DropboxSetup.constants = DropboxSetup(appKey: CloudAccessSecrets.dropboxAppKey, sharedContainerIdentifier: nil, keychainService: CryptomatorConstants.mainAppBundleId, forceForegroundSession: true) GoogleDriveSetup.constants = GoogleDriveSetup(clientId: CloudAccessSecrets.googleDriveClientId, redirectURL: CloudAccessSecrets.googleDriveRedirectURL!, sharedContainerIdentifier: nil) do { - let oneDriveConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.oneDriveClientId, redirectUri: CloudAccessSecrets.oneDriveRedirectURI, authority: nil) - oneDriveConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId - let oneDriveClientApplication = try MSALPublicClientApplication(configuration: oneDriveConfiguration) - OneDriveSetup.constants = OneDriveSetup(clientApplication: oneDriveClientApplication, sharedContainerIdentifier: nil) + let microsoftGraphConfiguration = MSALPublicClientApplicationConfig(clientId: CloudAccessSecrets.microsoftGraphClientId, redirectUri: CloudAccessSecrets.microsoftGraphRedirectURI, authority: nil) + microsoftGraphConfiguration.cacheConfig.keychainSharingGroup = CryptomatorConstants.mainAppBundleId + let microsoftGraphClientApplication = try MSALPublicClientApplication(configuration: microsoftGraphConfiguration) + MicrosoftGraphSetup.constants = MicrosoftGraphSetup(clientApplication: microsoftGraphClientApplication, sharedContainerIdentifier: nil) } catch { DDLogError("Setting up OneDrive failed with error: \(error)") } diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json new file mode 100644 index 000000000..82caa1afe --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint-vault-selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint-vault-selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint-vault-selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd9b460c3e40a0baabd6c12ecbd63ee9b322bf9 GIT binary patch literal 1315 zcmV+;1>E|HP)!)A%l$aN-#Pc3 za}UBQ5)OyO@Ydm{P~ejiWI2K;C7=xmDEK=KfD?!`%v<|4pT93!6V?mqL#u`A{%X5t z#&eheju6sjSc4G4B(rGIO8jT6fb#+d&j+`WfKP#-5^h7-`cWWyInofhmb;b%7TQl} zoaUhq?m$@a3==-Ypl4Ui-;%lu!TmzQw>-4N|0B92ICyy(=n|lEBOvy3Alw6%tWg5e zIFBZYq>lq=d!hXc5<`L=i|#c6Re-FUb^pl2a_e&#kv43;9J)BZIK$^$u37i%h;WkK z=PCd?G*{w(LIslPI(MwNi8$n~v8Zj%ryX$J@Ky#o1K;t)V+`;w#IbFzq~ZflY-B8#l(ua}lwG{fk;O&eSCRVe7nn#3?>%bC z8Sr=io{C4Yt~0ocbEn60qX1SIVTHQ~In*D|Ps&zDl+jR#K`0;`0Rg*>pq6sk4BKqt zm>OsF%-`zEq>c-oRAWPzukfZCBK$}San6@^P61zG+>v}!03K77C@(LhovJ)ljYAwlcQo-X@A&2iMVIjE=#oW^GYGP@EE?PT1cHF--s9~-DwA>yBq z-(6+Q7`K3hsQXPgk^^ktW@$iqG#WMUue-7PTnbVEX&hnNr$i2>~y^Dr) z5K$`O$SkGArH07!nlJeft9gSJrI#I>nzt5mDv)~4)E!P*0hW!IrN|KNAmBPWAk}$E!3S7-ZLUU|u(NN*xQ13|Gl9}zXZ`ju zF(!-58QYYRG-4`;m9@Ab*QFz? zCjO#ue!VJmaYeTqzpY^1(X+RwL5=pE-M2pB)ZSq$kL1(AH8&ute)jjOvIDCKTXcW? zzW86fcPeWhaZFjxZvK)}=nqRUfJx1#_1QSL=#ghPae6?YAR@xX;*{r1hB8-$zz2E8 z&Wt}VzUg@n((n>T^4kE`F9u4{{C%)@*sSiU673Ur%zc0gw-g)GptqBa4n_7vmdJ69 zeUZ(4-@JKi8P)Dhd@;{^K^FR$0Y0}{Ymw&sy8^0Xli+8|R%@_kIJj!E9^gf(JI^f! z-rW{MEMHX%%;q2m#thu4pxIhvf#m=yF}p0H8V-n2xP2MooZPXpW1Ht;A!a8Kb2ssp zqnr|@um+h=FKX2|2)WR&y%Ej>yp!*umKE~bNn6wlvrzP>&C*CPCI$6X)%$zH7?y?f Z{Rd&s4_)GSN-_Wd002ovPDHLkV1j`)Y|H=v literal 0 HcmV?d00001 diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a90e609f4d64c247fc18c3fd183ba1660cb5c3e GIT binary patch literal 3091 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 literal 0 HcmV?d00001 diff --git a/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png b/SharedResources/Assets.xcassets/sharepoint-selected.imageset/sharepoint-vault-selected@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d1ef12e5da9938250163c42abbbffc3e221c42 GIT binary patch literal 2794 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 literal 0 HcmV?d00001 diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json new file mode 100644 index 000000000..8da75771a --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint-vault.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint-vault@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint-vault@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault.png new file mode 100644 index 0000000000000000000000000000000000000000..1763f39f86233ca5e387642b03dd23140a5c5a24 GIT binary patch literal 1645 zcmV-z29o)SP)X-|&v|~&dHyp^ zF=}gTm4A`{DOSANB5Ns=N)wWqOsp{9AmYo+;s4x;`zEAF(X%+nha*i*0_?BB++ zpM_SAsFz37krI+dP&*k*CNoMe16uSzM^gGAv47e|8;q!j;%M7=X^SdMvW{aJP!_Lqh8zzwJ2S|X-LTi2 z*ZZ!2o84CH9D7e(<{LIoqGd_9&Q;qo>enUqCH)Llriob3wU)IuMqjFItDJVbZ{Rm9 zFHH+26(6lS8j3#8cN;Ij1YauQtu19OyT%J!umLcKv;$_AWGqk*^e5(OFl=54&IleJ zdtsn=paRouaaeU@!SrY?^B%->!cw%BVvMY=tVV!!10&V%MglQB#s^Rf)F}17Qr|n8 zD9>G#sYn{^8!Qjue;$n8A;NZOSS))Tadb+;v=!c1j=OVhQc@o}6gM=jX}S>&3Wt0j zUjG(iv%v}E3{5KJ=olezkhKs5PYi$F#Q8B1iHGV8&?^V?-$(%UxR#=rzOsb>f52q|q(MNQsW%4`2Cu~cR6{N;pxhFRHfXTX6P7w} zxwj{pSjQS@ci@I$$Mx_SOc~`pQ)m3iQkHUr=?rq|9H-Nf10kc-7)+QQ128~}YGC1zsmyp5j`C9Lkhtj20#D)2$|IGoQ3=h611X0Z z%|Ii(bvXkdciZSePLBZ?5305Jp(;>QBz$aZeB%(6#iHuL=C#}LfqK2KQ=cg7c`UCX z^_?13M#!;fcy-wcjb1@aTNAh&iuMm4a!m2zv@fRNu*CWwjwoOnYZ7`zlKyu)uT`lrG#w%Ftwt-z%Mywi` z?<<|B?hIW-LimMEskhkG&2*=VS=FKFe&H+eR*$`7*TxW^BVCX!RqZWr{?x7(!Rdid z@Z3(F?>Ovei1onM0?$=sfrtaahRmTqVEY{qpmFd{(zcjd8B#kn7=Gsi58nPX}LQ zecXHQJ@=lE-#Pa&nnWr&QKGxO?s-zk0+O^qF!4E(tPrne;x~_&bgOoZSWuF@jLUw; zSSJbjU&dLxLB>UY?#$QQmhJY_BoU)Ypj)54)$Vio*RzmVB1kS}k`+LJ+^m=|8ib%1 ziL4KNjI3ve7P2x##Un_>L=JU0oeNy|8^gH^kGpy*{xs3&A^{ zBqk2Jg09D*LP44Yf;V>O47jT$6KgQR6*P9z9o*pGDOy+5QOD{;I$j!$Lpk?NUB-p1 z$K`^gR|8DY(*Z$0S+chH`R#MJPsj9D3_?cwsp;|cG{S32c2(CW^Bc2+<@?KjA~|ivr6c-C+1#I6*Bts5Wk^Z` zxQ%a=f^M^lmyy9)FFfUg6rv{2`pn#Ze2jyuo*^kWP<)eMkBsU(YYuNSw@a`=g-{?| zEsYANW^ocW_b_EhmHR7ihT%Udg#1}L;S}LLKsbMxG}cglxO^sMLHY>4UpYGyH#aoo zE@(}H&xr4!$^@JP?q6UGfq2pIFaVX*bgU%IFu;)x!!vOw{Ng+;4shu*I@Y*=d zluQ=F1FnbDxXIME8`t-?u4z3&8B*B05`W2Tj}ZEi8M)K9?vA3JxPZ6pUo}_bau+U( zOgtXuLM}<7=3Oc`< z4dyIws@Q>8c_&3x6)31%hIoxR|jS0skJ1-o@ zmpyAeze^>YBIWVmk$3BYmtp75N5+)JUYqZiDpR2hiG^f6lcTDtz(2Ez+Sq+qz_9I6 z*kDIHN_lWFcd&;tpw+;U=pWm;w+(L8A1yuFpGn_^oeNJ%gFhG_m$Q}cAPu7oX;Igr z9t9N>9tv8nJh-|H3+T=nE`{WABUj$do#)=&wybSj_@xlf2!%FM)G;C9-Q&;oH&KR! z{^H2G!}}G0Q1@yFxJ{I0(_i6W1@>dlu-LPGc3*hIxQcs1&{q??$s@7UW;{B5{b?9D zi|2$)YR9lJ*_Rvy`z!v37ZgGkXVWzd@+CWNeg@lKTz8^Q*L(AOuVX?hHbulf?CZW% zjG&71P-oekz(~qGVVV%#^ccadGV^ zPbvmtsYY$*Sf=rj8L|cCj&<rTaFL>k|~=y#?Y z2$TD!A;;ogjabp;fy);VPP(QJOkExl#{E#s`9g|R4kcfCSHQ(Hw=}KVx@6j-v*YHL za!MDmKo_o-MxPXfMq~jum{guK*EEy%<~7Y1Q!^1+1>=OSFm?Q!>AdRP6JmyP3OC{2 z0nNdVvWr;Q7xC;2?%z~ZHx)Xtvnw=`FPwe9eMiYSp4IoU{!19+RnvgET$5ko$QVAA z)b=&XjzXAFJ6tu81>J){`vWllO=Oa038e#zcRMEWH5x@qDpuA?&8R`?qDJRh4J-v! z1KJ!GIzkQrg*zMrmv2)xWi<)q5aBTsG!NCiLLDnNFTB1{;)1#;3&Mo%8JgzX>lk!g zM^c}~1oLw{YZ4);>~OlN;Vex2j_92qi*YXsNqbWSbv4P8fNsrfgxv+NyhRC#_Vs55 z&M7tBYc&_xUzbS~<~8X+wLRL(V+x*e*t57t_Qp844pJw`DJc zgrVG_-RTfZ>#EjXXVCd;EKCf#P3MemPd6JO#HFN1Gtr05Fh6m+oR=wMlJ>lTJcrBY z>W4aX;z1Bzb+(Kcx`oZiHl4KlGYKVX0Z_UK^A5O109Z%U(%Jt<<<@Z(`s3@sAP?eVncX(#6X~Hh6 zb|uf2B+oqvw<{IekH2`|_P+9bnW-rbOps*m$AyZeD$i5)tZJtnaA8BKx=2R%sbA!V zWXKUO7G#^j^}R=}VsCgDW1cZ3cs-26wW(czzCA!SveiKv)fidGl6gZZkvOQeX z;AAKq!vK7np#}L^_T?Eca6{mqYtm>~d1PgNswN66BkR90K0ikPvE-+ZsfPE%$*_c# z6Ov`D}QHdljnqS#w*J-Q)kSprdOs$ak;+e+C2Pv32Dg zwSa9;si&$Sd5pfiJ|G@h620|nTsvSjA0a7W?%2Fe6($k{WKzsYhZvGbdEhFkDqKwN z1e9xlf+(yyi^ePl^`VA&5J_;8XPxIMLQ={|+Z^)&a7-(-k&%Qrr*b+#$$J$TkMel2 zrLyIc)nquwA&z-WCNFVe$X9SZINFMR@!XYjvn@DS>i@VTrJrj&9{Z5d54Nstc^2!w z34y;);ZhW;?u$7&<*a%!CQK2Y%j;HdDw_5c)J$?&dI%#_m8({-PcmB+mo_t}$5(v$5yl``d9@ zioblX-MbET7HEd7HP>b(84dnxd}pkN4XL9yL8h}xcG0fcFY1AOZ>Qs`^KWnv^P+F^ z4Jj+}+nmekSN0BAfP;tDp}g}NqDUD8SkEDcjs|OCZ z!L#njZ`1K1VIZ#@#lDQ>SpbUHc?5;?Q|7jC{qdScwuF_0(}+~Kcv6hDBG~XqN><2H zNNTl1*9>h>*I(6?;898wc!u~33M7P)j4PPah#ed?UPhmVH@v$})@6@$1nY-dh6_#* zbisJQ0h#@DwEL3jQ4<3DFHzB3SUhdfV$a%^=hNX% zrt;$uvO3lX_-{?;zRp9G!WPpJ7q%YB13kE$tt!{U_F=WMjaKbL2HnhYQ(E zS=|!Gvdv?yWDHFioKk=a;2LvyhekBUFm-%~7}s`&tIP2Rv~<|!{}E+Lhd1nh6Gf~Y zv3uB7eR*?bZE{AgYM*5X%5Fi6CuJn8gy6-VhzXY|X=M9>%KS}_+POhHV>j`5H$3(I z4mg_@>|)Fl=|C#Cp*-G@?OCAX8rzY;)u3`nYG(?NsMWa!-<`Ya;Hs05_%*4?{C5W5 zT?@OZ!uv3Sfm*jSjX#;qII46H44hQYrNh2!eDRG+I9&>#jbC-XjLXqO6Gq2%rftH? zc2gFlJ}ThSsDr|!O;VW(K9V{F*S?+(k0w)PZ4Jv2qy>YFUi8q$#h-(JVi2B`e!;Js$R33%ZN&he0Z@$iPj zSvfJ4Vg=X;e1l9lX+}ue>YL_!B-!Iy9$^Eq93|6dBWO4PV7?s*?a%2Wq_U<}4`MFg zO*+Evz!7$3sv~U6uPj#Xt5|>nQUe++N$T(iAo_>1waE`km5r!`+h8hE-e!Id4}3~$ zeDl%DL+4ZVvkc^m|54Nd^WT!3@q^(vLeh9lc`*8rNR1V;8IMJetW5;0%e5_wD~dX- z`i}dh6OhJoLx=~U5=)X^^`Uno^73tg+{HZ;{Tk4U)TvcxQD=eQ2>wXzT9QhJcMeAm zTb^I-Ii8NsxKM22`GswU#?*mJ@+wWoD0kyDX@}F>UOTZ`PHIv(_}qe*H~9_!?-A$j zOcoAwK{xp3C6za}jmImCkMWe_;orvEsn0i3)jWF<4vye>cb_ix?&d7qzQU9FSMp~E zY=OB_WilvtBv%BY(^ckDPj>Otq`lf6C|ZpuJ>GXfOcy|#Jf%SXpK@&>=M-U z1o%HbJaxEZLZ`;`0R%Kt1jD*Z0V{D7&WZuQ3bXzwJyI9y;p$7qkT~r!T#b|Q{pq9# z7^AR_UU6Kf*|b7K$RZ7AkJw#R3dvW8Jfr6z%2|X9&b`n<9DdRpDDRzTA=XorU({JQ bi6Q5IZ%(iiWD|BX00000NkvXXu0mjfJMNXN literal 0 HcmV?d00001 diff --git a/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png b/SharedResources/Assets.xcassets/sharepoint-vault.imageset/sharepoint-vault@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f01cff15e783ce40fd996d3a468c265a67a6e2ba GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json b/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json new file mode 100644 index 000000000..f59632c0d --- /dev/null +++ b/SharedResources/Assets.xcassets/sharepoint.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "sharepoint.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "sharepoint@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "sharepoint@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png b/SharedResources/Assets.xcassets/sharepoint.imageset/sharepoint.png new file mode 100644 index 0000000000000000000000000000000000000000..2f796b363d7d83a9213bb033d54c39d545b35911 GIT binary patch literal 1083 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 Date: Thu, 2 Jan 2025 11:14:59 +0100 Subject: [PATCH 2/7] Fixed build errors --- .../AccountListViewModel.swift | 16 +++++------- Cryptomator/Common/CloudAuthenticator.swift | 25 ++++++++++--------- .../VaultDetailInfoFooterViewModel.swift | 11 ++++---- .../Manager/CloudProviderDBManager.swift | 18 ++++++------- .../scripts/create-cloud-access-secrets.sh | 6 ++--- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift index a27867d50..ef2dd81f3 100644 --- a/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift +++ b/Cryptomator/Common/CloudAccountList/AccountListViewModel.swift @@ -75,6 +75,7 @@ class AccountListViewModel: AccountListViewModelProtocol { } } + // swiftlint:disable:next cyclomatic_complexity func createAccountCellContent(from accountInfo: AccountInfo) throws -> AccountCellContent { switch cloudProviderType { case .box: @@ -87,10 +88,7 @@ class AccountListViewModel: AccountListViewModelProtocol { case .localFileSystem: throw AccountListError.unsupportedCloudProviderType case .oneDrive: - let credential = try OneDriveCredential(with: accountInfo.accountUID) - return try createAccountCellContent(for: credential) - case .sharePoint: - let credential = try SharePointCredential(with: accountInfo.accountUID) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountInfo.accountUID) return try createAccountCellContent(for: credential) case .pCloud: return createAccountCellContentPlaceholder() @@ -100,6 +98,9 @@ class AccountListViewModel: AccountListViewModelProtocol { } let displayName = try S3CredentialManager.shared.getDisplayName(for: credential) return createAccountCellContent(for: credential, displayName: displayName) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: accountInfo.accountUID) + return try createAccountCellContent(for: credential) case .webDAV: guard let credential = WebDAVCredentialManager.shared.getCredentialFromKeychain(with: accountInfo.accountUID) else { throw CloudProviderAccountError.accountNotFoundError @@ -123,12 +124,7 @@ class AccountListViewModel: AccountListViewModelProtocol { return AccountCellContent(mainLabelText: username, detailLabelText: nil) } - func createAccountCellContent(for credential: OneDriveCredential) throws -> AccountCellContent { - let username = try credential.getUsername() - return AccountCellContent(mainLabelText: username, detailLabelText: nil) - } - - func createAccountCellContent(for credential: SharePointCredential) throws -> AccountCellContent { + func createAccountCellContent(for credential: MicrosoftGraphCredential) throws -> AccountCellContent { let username = try credential.getUsername() return AccountCellContent(mainLabelText: username, detailLabelText: nil) } diff --git a/Cryptomator/Common/CloudAuthenticator.swift b/Cryptomator/Common/CloudAuthenticator.swift index 07fdf53a7..982486272 100644 --- a/Cryptomator/Common/CloudAuthenticator.swift +++ b/Cryptomator/Common/CloudAuthenticator.swift @@ -43,20 +43,20 @@ class CloudAuthenticator { } } - func authenticateMicrosoftGraph(from viewController: UIViewController, providerType: CloudProviderType) -> Promise { - return MicrosoftGraphAuthenticator.authenticate(from: viewController, for: providerType).then { credential -> CloudProviderAccount in - let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: providerType) + func authenticateOneDrive(from viewController: UIViewController) -> Promise { + return MicrosoftGraphAuthenticator.authenticateForOneDrive(from: viewController).then { credential -> CloudProviderAccount in + let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .oneDrive) try self.accountManager.saveNewAccount(account) return account } } - func authenticateOneDrive(from viewController: UIViewController) -> Promise { - return authenticateMicrosoftGraph(from: viewController, providerType: .oneDrive) - } - func authenticateSharePoint(from viewController: UIViewController) -> Promise { - return authenticateMicrosoftGraph(from: viewController, providerType: .sharePoint) + return MicrosoftGraphAuthenticator.authenticateForSharePoint(from: viewController).then { credential -> CloudProviderAccount in + let account = CloudProviderAccount(accountUID: credential.identifier, cloudProviderType: .sharePoint) + try self.accountManager.saveNewAccount(account) + return account + } } func authenticatePCloud(from viewController: UIViewController) -> Promise { @@ -120,6 +120,7 @@ class CloudAuthenticator { } } + // swiftlint:disable:next cyclomatic_complexity func deauthenticate(account: CloudProviderAccount) throws { switch account.cloudProviderType { case .box: @@ -135,16 +136,16 @@ class CloudAuthenticator { case .localFileSystem: break case .oneDrive: - let credential = try OneDriveCredential(with: account.accountUID) - try credential.deauthenticate() - case .sharePoint: - let credential = try SharePointCredential(with: account.accountUID) + let credential = MicrosoftGraphCredential.createForOneDrive(with: account.accountUID) try credential.deauthenticate() case .pCloud: let credential = try PCloudCredential(userID: account.accountUID) try credential.deauthenticate() case .s3: try S3CredentialManager.shared.removeCredential(with: account.accountUID) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + try credential.deauthenticate() case .webDAV: try WebDAVCredentialManager.shared.removeCredentialFromKeychain(with: account.accountUID) } diff --git a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift index d7052fd46..b778ae133 100644 --- a/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailInfoFooterViewModel.swift @@ -42,6 +42,7 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode return String(format: LocalizedString.getValue("vaultDetail.info.footer.accountInfo"), username, vault.cloudProviderType.localizedString()) + " " } + // swiftlint:disable:next cyclomatic_complexity func getUsername() -> String? { switch vault.cloudProviderType { case .box: @@ -59,22 +60,22 @@ class VaultDetailInfoFooterViewModel: BindableAttributedTextHeaderFooterViewMode case .localFileSystem: return nil case .oneDrive: - let credential = try? OneDriveCredential(with: vault.delegateAccountUID) - return try? credential?.getUsername() + let credential = MicrosoftGraphCredential.createForOneDrive(with: vault.delegateAccountUID) + return try? credential.getUsername() case .pCloud: guard let credential = try? PCloudCredential(userID: vault.delegateAccountUID) else { return nil } getUsername(for: credential) return "(…)" - case .sharePoint: - let credential = try? SharePointCredential(with: vault.delegateAccountUID) - return try? credential?.getUsername() case .s3: guard let displayName = try? S3CredentialManager.shared.getDisplayName(for: vault.delegateAccountUID) else { return nil } return displayName + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: vault.delegateAccountUID) + return try? credential.getUsername() case .webDAV: let credential = WebDAVCredentialManager.shared.getCredentialFromKeychain(with: vault.delegateAccountUID) return credential?.username diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 932870fbf..07a2092c3 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -78,10 +78,7 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating } provider = try LocalFileSystemProvider(rootURL: rootURL, maxPageSize: .max) case .oneDrive: - let credential = try OneDriveCredential(with: accountUID) - provider = try OneDriveCloudProvider(credential: credential, maxPageSize: .max) - case .sharePoint: - let credential = try SharePointCredential(with: accountUID) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountUID) provider = try MicrosoftGraphCloudProvider(credential: credential, maxPageSize: .max) case .pCloud: let credential = try PCloudCredential(userID: accountUID) @@ -90,6 +87,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .s3: let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider(credential: credential) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: accountUID) + provider = try MicrosoftGraphCloudProvider(credential: credential, maxPageSize: .max) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient(credential: credential) @@ -132,11 +132,8 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating } provider = try LocalFileSystemProvider(rootURL: rootURL, maxPageSize: maxPageSizeForFileProvider) case .oneDrive: - let credential = try OneDriveCredential(with: accountUID) - provider = try OneDriveCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) - case .sharePoint: - let credential = try SharePointCredential(with: accountUID) - provider = try OneDriveCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) + let credential = MicrosoftGraphCredential.createForOneDrive(with: accountUID) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .pCloud: let credential = try PCloudCredential(userID: accountUID) let client = PCloud.createBackgroundClient(with: credential.user, sessionIdentifier: sessionIdentifier) @@ -144,6 +141,9 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating case .s3: let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider.withBackgroundSession(credential: credential, sharedContainerIdentifier: CryptomatorConstants.appGroupName) + case .sharePoint: + let credential = MicrosoftGraphCredential.createForSharePoint(with: accountUID) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient.withBackgroundSession(credential: credential, sessionIdentifier: sessionIdentifier, sharedContainerIdentifier: CryptomatorConstants.appGroupName) diff --git a/fastlane/scripts/create-cloud-access-secrets.sh b/fastlane/scripts/create-cloud-access-secrets.sh index 26af91b77..f243cf966 100755 --- a/fastlane/scripts/create-cloud-access-secrets.sh +++ b/fastlane/scripts/create-cloud-access-secrets.sh @@ -27,9 +27,9 @@ public enum CloudAccessSecrets { public static let googleDriveClientId = "${GOOGLE_DRIVE_CLIENT_ID}" public static let googleDriveRedirectURLScheme = "${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}" public static let googleDriveRedirectURL = URL(string: "${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}:/oauthredirect") - public static let oneDriveClientId = "${ONEDRIVE_CLIENT_ID}" - public static let oneDriveRedirectURIScheme = "${ONEDRIVE_REDIRECT_URI_SCHEME}" - public static let oneDriveRedirectURI = "${ONEDRIVE_REDIRECT_URI_SCHEME}://auth" + public static let microsoftGraphClientId = "${MICROSOFT_GRAPH_CLIENT_ID}" + public static let microsoftGraphRedirectURIScheme = "${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}" + public static let microsoftGraphRedirectURI = "${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}://auth" public static let pCloudAppKey = "${PCLOUD_APP_KEY}" } EOM From 2e557ead1886e6a7e36f659bc990b433c492f794 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Fri, 3 Jan 2025 10:40:52 +0100 Subject: [PATCH 3/7] Fixed runtime errors --- Cryptomator.xcodeproj/project.pbxproj | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 800a64997..072b73c20 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -2495,7 +2495,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${ONEDRIVE_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; + shellScript = "if [ -f ./fastlane/scripts/.cloud-access-secrets.sh ]; then\n source ./fastlane/scripts/.cloud-access-secrets.sh \"${CONFIG_NAME}\"\nelse\n echo \"warning: .cloud-access-secrets.sh could not be found, please see README for instructions\"\nfi\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:0 string db-${DROPBOX_APP_KEY}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:1 string ${GOOGLE_DRIVE_REDIRECT_URL_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:2 string ${MICROSOFT_GRAPH_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n/usr/libexec/PlistBuddy -c \"Add :CFBundleURLTypes:1:CFBundleURLSchemes:3 string ${HUB_REDIRECT_URI_SCHEME}\" \"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\necho \"Updated ${TARGET_BUILD_DIR}/${INFOPLIST_PATH} by adding URL schemes\"\n"; }; 742595D72552EE0000A8A008 /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; diff --git a/README.md b/README.md index d68fa6649..dc3270140 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ export BOX_CLIENT_SECRET=... export DROPBOX_APP_KEY=... export GOOGLE_DRIVE_CLIENT_ID=... export GOOGLE_DRIVE_REDIRECT_URL_SCHEME=... -export ONEDRIVE_CLIENT_ID=... -export ONEDRIVE_REDIRECT_URI_SCHEME=... +export MICROSOFT_GRAPH_CLIENT_ID=... +export MICROSOFT_GRAPH_REDIRECT_URI_SCHEME=... export PCLOUD_APP_KEY=... ``` From 2f3c0703e3fe34b9a5d4633479e2d663b198d538 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 6 Jan 2025 15:23:00 +0100 Subject: [PATCH 4/7] Add SharePoint drive selection. --- .../CreateNewVaultCoordinator.swift | 44 ++++++++--- .../EnterSharePointURLViewController.swift | 7 +- .../SharePointDriveListViewController.swift | 69 +++++++++++++++++ .../SharePointDriveListViewModel.swift | 75 +++++++++++++++++++ .../CreateNewVault/URLValidator.swift | 16 +++- .../OpenExistingVaultCoordinator.swift | 54 ++++++++++++- SharedResources/en.lproj/Localizable.strings | 4 + 7 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift create mode 100644 Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index 2a425edc6..7ef16725c 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -56,18 +56,9 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA navigationController.pushViewController(enterURLVC, animated: true) } - func setSharePointURL(_ url: String) { - guard let account = currentSharePointAccount else { return } - do { - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - startFolderChooser(with: provider, account: account.cloudProviderAccount) - } catch { - handleError(error, for: navigationController) - } - } - func selectedAccont(_ account: AccountInfo) throws { if account.cloudProviderType == .sharePoint { + currentSharePointAccount = account showEnterSharePointURL(for: account) } else { let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) @@ -75,6 +66,39 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA } } + func setSharePointURL(_ url: String) { + guard let account = currentSharePointAccount else { return } + + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let discovery = MicrosoftGraphDiscovery(credential: credential) + + showDriveList(discovery: discovery, sharePointURL: url) + } + + private func showDriveList(discovery: MicrosoftGraphDiscovery, sharePointURL: String) { + guard let account = currentSharePointAccount else { return } + let viewModel = SharePointDriveListViewModel(discovery: discovery, sharePointURL: sharePointURL, account: account) + viewModel.didSelectDrive = { [weak self] drive in + self?.handleDriveSelection(drive: drive) + } + let driveListVC = SharePointDriveListViewController(viewModel: viewModel) + navigationController.pushViewController(driveListVC, animated: true) + } + + private func handleDriveSelection(drive: MicrosoftGraphDrive) { + guard let account = currentSharePointAccount else { + print("No current SharePoint account available") + return + } + do { + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } catch { + handleError(error, for: navigationController) + } + } + private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) { let child = AuthenticatedCreateNewVaultCoordinator(navigationController: navigationController, provider: provider, account: account, vaultName: vaultName) childCoordinators.append(child) diff --git a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift index 2a8b359b8..c7694ed2c 100644 --- a/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift +++ b/Cryptomator/AddVault/CreateNewVault/EnterSharePointURLViewController.swift @@ -28,10 +28,13 @@ class EnterSharePointURLViewController: SingleSectionStaticUITableViewController } @objc func nextButtonClicked() { + guard let coordinator = coordinator else { return } do { - try coordinator?.setSharePointURL(viewModel.getValidatedSharePointURL()) + let url = try viewModel.getValidatedSharePointURL() + coordinator.setSharePointURL(url) } catch { - coordinator?.handleError(error, for: self) + print("Error validating SharePoint URL: \(error)") + coordinator.handleError(error, for: self) } } diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift new file mode 100644 index 000000000..d1aecb92c --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewController.swift @@ -0,0 +1,69 @@ +// +// SharePointDriveListViewController.swift +// Cryptomator +// +// Created by Majid Achhoud +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation +import UIKit + +class SharePointDriveListViewController: BaseUITableViewController { + private var viewModel: SharePointDriveListViewModel + + init(viewModel: SharePointDriveListViewModel) { + self.viewModel = viewModel + super.init() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + tableView.register(CloudCell.self, forCellReuseIdentifier: "SharePointDriveCell") + viewModel.reloadData = { [weak self] in + self?.tableView.reloadData() + } + + self.title = LocalizedString.getValue("addVault.selectDrive.navigation.title") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.drives.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "SharePointDriveCell", for: indexPath) as? CloudCell else { + fatalError("Could not dequeue CloudCell") + } + + let drive = viewModel.drives[indexPath.row] + configure(cell, with: drive) + + return cell + } + + // MARK: - Styling Configuration + + private func configure(_ cell: CloudCell, with drive: MicrosoftGraphDrive) { + cell.textLabel?.text = drive.name + cell.imageView?.image = UIImage(systemName: "folder") + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let selectedDrive = viewModel.drives[indexPath.row] + viewModel.selectDrive(selectedDrive) + tableView.deselectRow(at: indexPath, animated: true) + } +} + diff --git a/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift new file mode 100644 index 000000000..4cc369e49 --- /dev/null +++ b/Cryptomator/AddVault/CreateNewVault/SharePointDriveListViewModel.swift @@ -0,0 +1,75 @@ +// +// SharePointDriveListViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import Foundation + +class SharePointDriveListViewModel: SingleSectionTableViewModel { + private let discovery: MicrosoftGraphDiscovery + private let sharePointURL: String + private let account: AccountInfo + + var drives: [MicrosoftGraphDrive] = [] { + didSet { + reloadData?() + } + } + + var reloadData: (() -> Void)? + var didSelectDrive: ((MicrosoftGraphDrive) -> Void)? + + init(discovery: MicrosoftGraphDiscovery, sharePointURL: String, account: AccountInfo) { + self.discovery = discovery + self.sharePointURL = sharePointURL + self.account = account + super.init() + fetchSiteAndDrives() + } + + func selectDrive(_ drive: MicrosoftGraphDrive) { + didSelectDrive?(drive) + } + + private func fetchSiteAndDrives() { + guard let urlComponents = URL(string: sharePointURL), + let hostName = urlComponents.host else { + print("Invalid SharePoint URL") + return + } + + var serverRelativePath = urlComponents.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + if serverRelativePath.hasPrefix("sites/") { + serverRelativePath = String(serverRelativePath.dropFirst("sites/".count)) + } + + discovery.fetchSharePointSite(for: hostName, serverRelativePath: serverRelativePath) + .then { site in + self.fetchDrives(for: site.identifier) + }.catch { error in + print("Failed to fetch SharePoint site: \(error)") + } + } + + private func fetchDrives(for siteIdentifier: String) { + discovery.fetchSharePointDocumentLibraries(for: siteIdentifier).then { drives in + self.drives = drives + }.catch { error in + print("Failed to fetch drives: \(error)") + } + } + + override func getHeaderTitle(for section: Int) -> String? { + guard section == 0 else { return nil } + return LocalizedString.getValue("addVault.selectDrive.navigation.title") + } + + override var title: String? { + return LocalizedString.getValue("addVault.selectDrive.header.description") + } +} diff --git a/Cryptomator/AddVault/CreateNewVault/URLValidator.swift b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift index 97cbd527d..87ed27bc0 100644 --- a/Cryptomator/AddVault/CreateNewVault/URLValidator.swift +++ b/Cryptomator/AddVault/CreateNewVault/URLValidator.swift @@ -24,10 +24,18 @@ extension URLValidatorError: LocalizedError { public enum URLValidator { public static func validateSharePointURL(urlString: String) throws { - let pattern = #"^https:\/\/[a-zA-Z0-9\-]+\.sharepoint\.com\/sites\/[a-zA-Z0-9\-]+$"# - let regex = try NSRegularExpression(pattern: pattern) - let range = NSRange(location: 0, length: urlString.utf16.count) - if regex.firstMatch(in: urlString, options: [], range: range) == nil { + guard let url = URL(string: urlString) else { + throw URLValidatorError.invalidURLFormat + } + + guard url.scheme == "https", + let host = url.host, + host.contains(".sharepoint.com") else { + throw URLValidatorError.invalidURLFormat + } + + let path = url.path + guard path.contains("/sites/") else { throw URLValidatorError.invalidURLFormat } } diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift index f9d5f8756..7a0a41c74 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultCoordinator.swift @@ -15,11 +15,13 @@ import Foundation import Promises import UIKit -class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator { +class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditAccountBehavior, Coordinator, SharePointURLSetting { var navigationController: UINavigationController var childCoordinators = [Coordinator]() weak var parentCoordinator: AddVaultCoordinator? + private var currentSharePointAccount: AccountInfo? + init(navigationController: UINavigationController) { self.navigationController = navigationController } @@ -43,6 +45,13 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } } + func showEnterSharePointURL(for account: AccountInfo) { + let viewModel = EnterSharePointURLViewModel(account: account) + let enterURLVC = EnterSharePointURLViewController(viewModel: viewModel) + enterURLVC.coordinator = self + navigationController.pushViewController(enterURLVC, animated: true) + } + func showAddAccount(for cloudProviderType: CloudProviderType, from viewController: UIViewController) { let authenticator = CloudAuthenticator(accountManager: CloudProviderAccountDBManager.shared) authenticator.authenticate(cloudProviderType, from: viewController).then { account in @@ -52,8 +61,47 @@ class OpenExistingVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEd } func selectedAccont(_ account: AccountInfo) throws { - let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) - startFolderChooser(with: provider, account: account.cloudProviderAccount) + if account.cloudProviderType == .sharePoint { + currentSharePointAccount = account + showEnterSharePointURL(for: account) + } else { + let provider = try CloudProviderDBManager.shared.getProvider(with: account.accountUID) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } + } + + func setSharePointURL(_ url: String) { + guard let account = currentSharePointAccount else { return } + + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let discovery = MicrosoftGraphDiscovery(credential: credential) + + showDriveList(discovery: discovery, sharePointURL: url) + } + + private func showDriveList(discovery: MicrosoftGraphDiscovery, sharePointURL: String) { + guard let account = currentSharePointAccount else { return } + let viewModel = SharePointDriveListViewModel(discovery: discovery, sharePointURL: sharePointURL, account: account) + viewModel.didSelectDrive = { [weak self] drive in + self?.handleDriveSelection(drive: drive) + } + let driveListVC = SharePointDriveListViewController(viewModel: viewModel) + navigationController.pushViewController(driveListVC, animated: true) + } + + private func handleDriveSelection(drive: MicrosoftGraphDrive) { + guard let account = currentSharePointAccount else { + print("No current SharePoint account available") + return + } + do { + let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) + let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier) + startFolderChooser(with: provider, account: account.cloudProviderAccount) + } catch { + print("Error creating provider: \(error)") + handleError(error, for: navigationController) + } } private func startFolderChooser(with provider: CloudProvider, account: CloudProviderAccount) { diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 55f855508..913291944 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -70,6 +70,10 @@ "addVault.openExistingVault.downloadVault.progress" = "Downloading Vault…"; "addVault.openExistingVault.password.footer" = "Enter password for \"%@\"."; "addVault.openExistingVault.progress" = "Adding Vault…"; + +"addVault.selectDrive.navigation.title" = "Select Drive"; +"addVault.selectDrive.header.description" = "Select the SharePoint drive where the encrypted vault will be located."; + "addVault.success.info" = "Successfully added vault \"%@\".\nAccess this vault via the Files app."; "addVault.success.footer" = "If you haven't already, enable Cryptomator in the Files app."; From 8a197db08381b5134f2a82a46e43fa508d8ceb3c Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Wed, 8 Jan 2025 14:39:29 +0100 Subject: [PATCH 5/7] WIP: Keychain implementation for sharepoint --- .../CreateNewVaultCoordinator.swift | 1 + .../CryptomatorKeychain.swift | 1 + .../Manager/CloudProviderDBManager.swift | 29 ++++-- .../MicrosoftGraphDriveManager.swift | 90 +++++++++++++++++++ 4 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift diff --git a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift index 7ef16725c..d1dbefa05 100644 --- a/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift +++ b/Cryptomator/AddVault/CreateNewVault/CreateNewVaultCoordinator.swift @@ -91,6 +91,7 @@ class CreateNewVaultCoordinator: AccountListing, CloudChoosing, DefaultShowEditA return } do { + try MicrosoftGraphDriveManager.shared.saveDriveToKeychain(drive, for: account.accountUID) let credential = MicrosoftGraphCredential.createForSharePoint(with: account.accountUID) let provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: drive.identifier) startFolderChooser(with: provider, account: account.cloudProviderAccount) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift index 20ce5215b..d83818dc7 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorKeychain.swift @@ -25,6 +25,7 @@ class CryptomatorKeychain: CryptomatorKeychainType { static let bundleId = CryptomatorConstants.mainAppBundleId static let box = CryptomatorKeychain(service: "box.auth") static let pCloud = CryptomatorKeychain(service: "pCloud.auth") + static let microsoftGraph = CryptomatorKeychain(service: "microsoftGraph.auth") static let s3 = CryptomatorKeychain(service: "s3.auth") static let webDAV = CryptomatorKeychain(service: "webDAV.auth") static let localFileSystem = CryptomatorKeychain(service: "localFileSystem.auth") diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift index 07a2092c3..96e8132da 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Manager/CloudProviderDBManager.swift @@ -6,6 +6,7 @@ // Copyright © 2020 Skymatic GmbH. All rights reserved. // +import Combine import CryptomatorCloudAccessCore import Foundation import PCloudSDKSwift @@ -30,11 +31,13 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating static var cachedProvider = [CachedProvider]() public static let shared = CloudProviderDBManager(accountManager: CloudProviderAccountDBManager.shared) let accountManager: CloudProviderAccountDBManager + let driveManager: MicrosoftGraphDriveManaging private let maxPageSizeForFileProvider = 500 - init(accountManager: CloudProviderAccountDBManager) { + init(accountManager: CloudProviderAccountDBManager, driveManager: MicrosoftGraphDriveManaging = MicrosoftGraphDriveManager.shared) { self.accountManager = accountManager + self.driveManager = driveManager } public func getProvider(with accountUID: String) throws -> CloudProvider { @@ -88,8 +91,12 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider(credential: credential) case .sharePoint: - let credential = MicrosoftGraphCredential.createForSharePoint(with: accountUID) - provider = try MicrosoftGraphCloudProvider(credential: credential, maxPageSize: .max) + let allDrives = try driveManager.getDrivesFromKeychain(for: accountUID) + guard let drive = allDrives.first else { + throw CloudProviderError.itemNotFound + } + let (credential, driveID) = try getSharePointCredentialAndDriveIdentifier(for: accountUID, driveID: drive.identifier) + provider = try MicrosoftGraphCloudProvider(credential: credential, driveIdentifier: driveID, maxPageSize: .max) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient(credential: credential) @@ -142,8 +149,12 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating let credential = try getS3Credential(for: accountUID) provider = try S3CloudProvider.withBackgroundSession(credential: credential, sharedContainerIdentifier: CryptomatorConstants.appGroupName) case .sharePoint: - let credential = MicrosoftGraphCredential.createForSharePoint(with: accountUID) - provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) + let allDrives = try driveManager.getDrivesFromKeychain(for: accountUID) + guard let drive = allDrives.first else { + throw CloudProviderError.itemNotFound + } + let (credential, driveID) = try getSharePointCredentialAndDriveIdentifier(for: accountUID, driveID: drive.identifier) + provider = try MicrosoftGraphCloudProvider.withBackgroundSession(credential: credential, driveIdentifier: driveID, maxPageSize: maxPageSizeForFileProvider, sessionIdentifier: sessionIdentifier) case .webDAV: let credential = try getWebDAVCredential(for: accountUID) let client = WebDAVClient.withBackgroundSession(credential: credential, sessionIdentifier: sessionIdentifier, sharedContainerIdentifier: CryptomatorConstants.appGroupName) @@ -159,6 +170,14 @@ public class CloudProviderDBManager: CloudProviderManager, CloudProviderUpdating return provider } + private func getSharePointCredentialAndDriveIdentifier(for accountUID: String, driveID: String) throws -> (MicrosoftGraphCredential, String) { + guard let drive = try driveManager.getDriveFromKeychain(for: accountUID, driveID: driveID) else { + throw CloudProviderError.itemNotFound + } + let credential = MicrosoftGraphCredential(identifier: drive.identifier, scopes: MicrosoftGraphScopes.sharePoint) + return (credential, drive.identifier) + } + private func getS3Credential(for accountUID: String) throws -> S3Credential { guard let credential = S3CredentialManager.shared.getCredential(with: accountUID) else { throw CloudProviderAccountError.accountNotFoundError diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift new file mode 100644 index 000000000..b9bb03fa4 --- /dev/null +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/MicrosoftGraph/MicrosoftGraphDriveManager.swift @@ -0,0 +1,90 @@ +// +// MicrosoftGraphDriveManager.swift +// CryptomatorCommon +// +// Created by Majid Achhoud on 07.01.25. +// + +import Foundation +import CryptomatorCloudAccessCore +import Combine + +public enum MicrosoftGraphDriveManagerError: Error { + case driveDuplicate(existingIdentifier: String) +} + +public protocol MicrosoftGraphDriveManaging { + var didUpdate: AnyPublisher { get } + + func getDrivesFromKeychain(for accountUID: String) throws -> [MicrosoftGraphDrive] + func saveDriveToKeychain(_ drive: MicrosoftGraphDrive, for accountUID: String) throws + func removeDriveFromKeychain(with accountUID: String, driveIdentifier: String) throws + func getDriveFromKeychain(for accountUID: String, driveID: String) throws -> MicrosoftGraphDrive? +} + +public struct MicrosoftGraphDriveManager: MicrosoftGraphDriveManaging { + public static let shared = MicrosoftGraphDriveManager(keychain: CryptomatorKeychain.microsoftGraph) + + public var didUpdate: AnyPublisher { + didUpdatePublisher.eraseToAnyPublisher() + } + + private let keychain: CryptomatorKeychainType + private let didUpdatePublisher = PassthroughSubject() + + public func getDrivesFromKeychain(for accountUID: String) throws -> [MicrosoftGraphDrive] { + return try keychain.getDriveIdentifiers(for: accountUID) ?? [] + } + + public func saveDriveToKeychain(_ drive: MicrosoftGraphDrive, for accountUID: String) throws { + var allDrives = try getDrivesFromKeychain(for: accountUID) + + if allDrives.contains(where: { $0.identifier == drive.identifier }) { + throw MicrosoftGraphDriveManagerError.driveDuplicate(existingIdentifier: drive.identifier) + } + + allDrives.append(drive) + try keychain.setDriveIdentifiers(allDrives, for: accountUID) + didUpdatePublisher.send(()) + } + + public func removeDriveFromKeychain(with accountUID: String, driveIdentifier: String) throws { + var allDrives = try getDrivesFromKeychain(for: accountUID) + allDrives.removeAll { $0.identifier == driveIdentifier } + try keychain.setDriveIdentifiers(allDrives, for: accountUID) + didUpdatePublisher.send(()) + } + + public func getDriveFromKeychain(for accountUID: String, driveID: String) throws -> MicrosoftGraphDrive? { + let allDrives = try getDrivesFromKeychain(for: accountUID) + return allDrives.first(where: { $0.identifier == driveID }) + } +} + +extension CryptomatorKeychainType { + func getDriveIdentifiers(for accountUID: String) throws -> [MicrosoftGraphDrive]? { + let driveKey = "driveIdentifiers_\(accountUID)" + guard let data = getAsData(driveKey) else { + return nil + } + do { + let jsonDecoder = JSONDecoder() + let drives = try jsonDecoder.decode([MicrosoftGraphDrive].self, from: data) + return drives + } catch { + return nil + } + } + + func setDriveIdentifiers(_ drives: [MicrosoftGraphDrive], for accountUID: String) throws { + let driveKey = "driveIdentifiers_\(accountUID)" + let jsonEncoder = JSONEncoder() + let encodedDrives = try jsonEncoder.encode(drives) + try set(driveKey, value: encodedDrives) + } + + func getDrive(by driveID: String, for accountUID: String) throws -> MicrosoftGraphDrive? { + let allDrives = try getDriveIdentifiers(for: accountUID) ?? [] + return allDrives.first(where: { $0.identifier == driveID }) + } +} From 054a90c38694ff57c4f52df781723db828a54118 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Wed, 8 Jan 2025 14:57:44 +0100 Subject: [PATCH 6/7] Add SharePointDriveListView to the project file --- Cryptomator.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 072b73c20..d4a051e02 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -437,6 +437,8 @@ B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */; }; B34C532A2D142BA700F30FE9 /* SharePointURLSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */; }; B34C532C2D142BF600F30FE9 /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C532B2D142BE000F30FE9 /* URLValidator.swift */; }; + B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */; }; + B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */; }; B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; }; /* End PBXBuildFile section */ @@ -1049,6 +1051,8 @@ B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = ""; }; B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointURLSetting.swift; sourceTree = ""; }; B34C532B2D142BE000F30FE9 /* URLValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLValidator.swift; sourceTree = ""; }; + B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewController.swift; sourceTree = ""; }; + B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewModel.swift; sourceTree = ""; }; B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1387,6 +1391,8 @@ 4A644B45267A3D21008CBB9A /* CreateNewVault */ = { isa = PBXGroup; children = ( + B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */, + B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */, B34C532B2D142BE000F30FE9 /* URLValidator.swift */, B34C53292D142B9200F30FE9 /* SharePointURLSetting.swift */, B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */, @@ -2789,6 +2795,7 @@ 4A4B7E7426B954D2009BFDB1 /* HeaderFooterViewModel.swift in Sources */, 4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */, 74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */, + B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */, 4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */, 4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */, 4AF4535F272066A600CF1919 /* RenameVaultViewController.swift in Sources */, @@ -2804,6 +2811,7 @@ 4AF91D0D25A8D5EF00ACF01E /* ListViewModel.swift in Sources */, 4A8D060525C82F1F0082C5F7 /* AddVaultSuccesing.swift in Sources */, 4A61F6B9274582E3007AA422 /* StaticUITableViewController.swift in Sources */, + B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */, 4A21B49426BC0127000D13DF /* BindableAttributedTextHeaderFooterViewModel.swift in Sources */, 740D367E266A18DF0058744D /* SettingsViewController.swift in Sources */, 4AF45356271F2A8300CF1919 /* RenameVaultViewModel.swift in Sources */, From abe88d3246aad646b8ac81c82fea5c6831a6e46b Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Fri, 17 Jan 2025 21:11:54 +0100 Subject: [PATCH 7/7] Improve Hub error handling and updates --- .../UnlockSectionFooterViewModel.swift | 13 +++++++----- .../VaultDetail/VaultDetailViewModel.swift | 4 ++-- .../Hub/CryptomatorHubAuthenticator.swift | 11 ++++++++-- .../Hub/HubAuthenticationView.swift | 4 +++- .../Hub/HubAuthenticationViewModel.swift | 5 ++++- .../HubDeviceRegisteredSuccessfullyView.swift | 19 ------------------ .../Hub/HubErrorWithRefreshView.swift | 20 +++++++++++++++++++ SharedResources/en.lproj/Localizable.strings | 5 +++-- 8 files changed, 49 insertions(+), 32 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..dda6c156d 100644 --- a/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift +++ b/Cryptomator/VaultDetail/UnlockSectionFooterViewModel.swift @@ -7,6 +7,7 @@ // import CryptomatorCommonCore +import CryptomatorCloudAccessCore import Foundation class UnlockSectionFooterViewModel: HeaderFooterViewModel { @@ -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..077550f94 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..78841b8d8 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,7 +80,9 @@ 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..4c6aecae0 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..c1b31c90d 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,7 +109,9 @@ public final class HubAuthenticationViewModel: ObservableObject { await setState(to: .licenseExceeded) case let .requiresAccountInitialization(profileURL): await delegate?.hubAuthenticationViewModelWantsToShowNeedsAccountInitAlert(profileURL: profileURL) - } + case .vaultArchived: + await setState(to: .vaultArchived) + } } private func receivedExistingKey(_ flowResponse: HubAuthenticationFlowSuccess) async { 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..776bee568 --- /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 913291944..f45ae3b66 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -127,16 +127,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.";