diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 1b1d0ed08..5481e75df 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -445,6 +445,9 @@ B34C532A2D142BA700F30FE9 /* SharePointAuthenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointAuthenticating.swift */; }; B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */; }; B379DBC12D27F5B5003B5849 /* SharePointDriveListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */; }; + B3C397FE2EB10FC0001280AC /* ShareVaultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C397FC2EB10FC0001280AC /* ShareVaultViewController.swift */; }; + B3C398002EB110F9001280AC /* ShareVaultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C397FF2EB110F9001280AC /* ShareVaultViewModel.swift */; }; + 988E23D6223540118B002237 /* ShareVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988E23D6223540118B002238 /* ShareVaultCoordinator.swift */; }; B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; }; /* End PBXBuildFile section */ @@ -1064,6 +1067,9 @@ B34C53292D142B9200F30FE9 /* SharePointAuthenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointAuthenticating.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 = ""; }; + B3C397FC2EB10FC0001280AC /* ShareVaultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultViewController.swift; sourceTree = ""; }; + B3C397FF2EB110F9001280AC /* ShareVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultViewModel.swift; sourceTree = ""; }; + 988E23D6223540118B002238 /* ShareVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultCoordinator.swift; sourceTree = ""; }; B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1282,6 +1288,7 @@ 4A4B7E3E26B2ABC0009BFDB1 /* VaultDetailViewController.swift */, 4A4B7E4126B2AD6F009BFDB1 /* VaultDetailViewModel.swift */, 4AB8539B26BA8A8200555F00 /* VaultPasswordVerifying.swift */, + B3C397FD2EB10FC0001280AC /* ShareVault */, 4AF45357271F2A8B00CF1919 /* RenameVault */, 4A0337C82726FBEC001753B7 /* MoveVault */, 4A91D8CF272ADC7E003F8BD8 /* ChangePassword */, @@ -2114,6 +2121,16 @@ path = Purchase; sourceTree = ""; }; + B3C397FD2EB10FC0001280AC /* ShareVault */ = { + isa = PBXGroup; + children = ( + 988E23D6223540118B002238 /* ShareVaultCoordinator.swift */, + B3C397FF2EB110F9001280AC /* ShareVaultViewModel.swift */, + B3C397FC2EB10FC0001280AC /* ShareVaultViewController.swift */, + ); + path = ShareVault; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2783,6 +2800,8 @@ 4A644B57267C958F008CBB9A /* ChildCoordinator.swift in Sources */, 4A53CC15267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift in Sources */, 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */, + B3C397FE2EB10FC0001280AC /* ShareVaultViewController.swift in Sources */, + 988E23D6223540118B002237 /* ShareVaultCoordinator.swift in Sources */, 4A136132276770BB0077EB7F /* SnapshotVaultListViewModel.swift in Sources */, 4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */, 747C35172762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift in Sources */, @@ -2810,6 +2829,7 @@ 4A644B55267C926A008CBB9A /* FolderCreating.swift in Sources */, 74A295EF2D80902800C54136 /* SharePointAuthenticator.swift in Sources */, 4A3D658626847B11000DA764 /* CreateNewLocalVaultViewModel.swift in Sources */, + B3C398002EB110F9001280AC /* ShareVaultViewModel.swift in Sources */, 4AE97DAB24572E4900452814 /* AppDelegate.swift in Sources */, 4AA22C16261CA8D800A17486 /* URLFieldCell.swift in Sources */, 4A2FD07925B5D98B008565C8 /* CloudCell.swift in Sources */, diff --git a/Cryptomator/VaultDetail/ShareVault/ShareVaultCoordinator.swift b/Cryptomator/VaultDetail/ShareVault/ShareVaultCoordinator.swift new file mode 100644 index 000000000..aa127785f --- /dev/null +++ b/Cryptomator/VaultDetail/ShareVault/ShareVaultCoordinator.swift @@ -0,0 +1,46 @@ +// +// ShareVaultCoordinator.swift +// Cryptomator +// +// Created by Majid Achhoud on 30.10.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCloudAccessCore +import CryptomatorCommonCore +import UIKit + +class ShareVaultCoordinator: Coordinator { + weak var parentCoordinator: Coordinator? + lazy var childCoordinators = [Coordinator]() + var navigationController: UINavigationController + private let vaultInfo: VaultInfo + + init(vaultInfo: VaultInfo, navigationController: UINavigationController) { + self.vaultInfo = vaultInfo + self.navigationController = navigationController + } + + func start() { + let viewModel: ShareVaultViewModel + if vaultInfo.vaultConfigType == .hub, let hubURL = extractHubVaultURL() { + viewModel = ShareVaultViewModel(type: .hub(hubURL)) + } else { + viewModel = ShareVaultViewModel(type: .normal) + } + let shareVaultViewController = ShareVaultViewController(viewModel: viewModel) + shareVaultViewController.coordinator = self + navigationController.pushViewController(shareVaultViewController, animated: true) + } + + private func extractHubVaultURL() -> URL? { + guard let cachedVault = try? VaultDBCache().getCachedVault(withVaultUID: vaultInfo.vaultUID), + let vaultConfigToken = cachedVault.vaultConfigToken, + let vaultConfig = try? UnverifiedVaultConfig(token: vaultConfigToken), + let hubConfig = vaultConfig.allegedHubConfig else { + return nil + } + + return hubConfig.getWebAppURL() + } +} diff --git a/Cryptomator/VaultDetail/ShareVault/ShareVaultViewController.swift b/Cryptomator/VaultDetail/ShareVault/ShareVaultViewController.swift new file mode 100644 index 000000000..7227eba70 --- /dev/null +++ b/Cryptomator/VaultDetail/ShareVault/ShareVaultViewController.swift @@ -0,0 +1,352 @@ +// +// ShareVaultViewController.swift +// Cryptomator +// +// Created by Majid Achhoud on 24.10.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import UIKit + +class ShareVaultViewController: UIViewController { + weak var coordinator: ShareVaultCoordinator? + private let viewModel: ShareVaultViewModelProtocol + + // MARK: - Layout Constants + + private enum LayoutConstants { + static let horizontalPadding: CGFloat = 32 + static let standardSpacing: CGFloat = 16 + static let largeSpacing: CGFloat = 32 + static let titleToSubtitleSpacing: CGFloat = 16 + static let subtitleToStepsSpacing: CGFloat = 20 + static let titleToFeaturesSpacing: CGFloat = 8 + static let titleSpacing: CGFloat = 24 + static let logoHeight: CGFloat = 44 + static let hubImageMultiplier: CGFloat = 0.7 + static let buttonHeight: CGFloat = 50 + static let iconSize: CGFloat = 24 + static let iconTextSpacing: CGFloat = 12 + static let stepsSpacing: CGFloat = 16 + static let cornerRadius: CGFloat = 12 + } + + // MARK: - UI Components + + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.alwaysBounceVertical = true + return scrollView + }() + + private lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + private lazy var logoImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: viewModel.logoImageName)) + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var hubImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "cryptomator-hub")) + imageView.contentMode = .scaleAspectFit + imageView.layer.cornerRadius = LayoutConstants.cornerRadius + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = viewModel.headerTitle + label.font = .preferredFont(forTextStyle: .title3) + label.numberOfLines = 0 + label.textAlignment = .center + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.text = viewModel.headerSubtitle + label.font = .preferredFont(forTextStyle: .subheadline) + label.numberOfLines = 0 + label.textAlignment = .left + label.textColor = .secondaryLabel + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var featuresLabel: UILabel = { + let label = UILabel() + label.text = viewModel.featuresText + label.font = .preferredFont(forTextStyle: .subheadline) + label.numberOfLines = 0 + label.textAlignment = .center + label.textColor = .secondaryLabel + label.adjustsFontForContentSizeCategory = true + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var hubStepsStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = LayoutConstants.stepsSpacing + stackView.alignment = .leading + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + private lazy var footerTextView: UITextView = { + let textView = UITextView() + textView.backgroundColor = .clear + textView.isEditable = false + textView.isScrollEnabled = false + textView.isUserInteractionEnabled = true + textView.textContainerInset = .zero + textView.textContainer.lineFragmentPadding = 0 + textView.translatesAutoresizingMaskIntoConstraints = false + textView.attributedText = createFooterAttributedText() + return textView + }() + + private lazy var visitHubButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle(viewModel.forTeamsButtonTitle, for: .normal) + button.backgroundColor = .cryptomatorPrimary + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + button.titleLabel?.adjustsFontForContentSizeCategory = true + button.layer.cornerRadius = LayoutConstants.cornerRadius + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(visitHubButtonTapped), for: .touchUpInside) + return button + }() + + init(viewModel: ShareVaultViewModelProtocol) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + title = viewModel.title + view.backgroundColor = .cryptomatorBackground + setupViews() + } + + private func setupViews() { + view.addSubview(scrollView) + view.addSubview(visitHubButton) + scrollView.addSubview(contentView) + contentView.addSubview(logoImageView) + contentView.addSubview(hubImageView) + contentView.addSubview(titleLabel) + + if viewModel.headerSubtitle != nil { + contentView.addSubview(subtitleLabel) + } + + if let hubSteps = viewModel.hubSteps { + setupHubStepsView(with: hubSteps) + contentView.addSubview(hubStepsStackView) + } else if viewModel.featuresText != nil { + contentView.addSubview(featuresLabel) + } + + if viewModel.footerText != nil { + contentView.addSubview(footerTextView) + } + + setupConstraints() + } + + private func setupHubStepsView(with steps: [(String, String)]) { + for (symbolName, text) in steps { + let stepView = createStepView(symbolName: symbolName, text: text) + hubStepsStackView.addArrangedSubview(stepView) + } + } + + private func createStepView(symbolName: String, text: String) -> UIView { + let containerView = UIView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + let symbolImageView = UIImageView() + symbolImageView.image = UIImage(systemName: symbolName) + symbolImageView.tintColor = .cryptomatorPrimary + symbolImageView.contentMode = .scaleAspectFit + symbolImageView.translatesAutoresizingMaskIntoConstraints = false + + let textLabel = UILabel() + textLabel.text = text + textLabel.font = .preferredFont(forTextStyle: .subheadline) + textLabel.numberOfLines = 0 + textLabel.textColor = .secondaryLabel + textLabel.adjustsFontForContentSizeCategory = true + textLabel.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(symbolImageView) + containerView.addSubview(textLabel) + + NSLayoutConstraint.activate([ + symbolImageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + symbolImageView.topAnchor.constraint(equalTo: textLabel.topAnchor), + symbolImageView.widthAnchor.constraint(equalToConstant: LayoutConstants.iconSize), + symbolImageView.heightAnchor.constraint(equalToConstant: LayoutConstants.iconSize), + + textLabel.leadingAnchor.constraint(equalTo: symbolImageView.trailingAnchor, constant: LayoutConstants.iconTextSpacing), + textLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + textLabel.topAnchor.constraint(equalTo: containerView.topAnchor), + textLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + return containerView + } + + private func setupConstraints() { + let contentLayoutGuide = scrollView.contentLayoutGuide + let frameLayoutGuide = scrollView.frameLayoutGuide + + var constraints = createBaseConstraints(contentLayoutGuide: contentLayoutGuide, frameLayoutGuide: frameLayoutGuide) + let topAnchor = addOptionalSubtitleConstraints(to: &constraints) + let lastContentView = addContentConstraints(to: &constraints, topAnchor: topAnchor) + addFooterConstraints(to: &constraints, lastContentView: lastContentView) + + NSLayoutConstraint.activate(constraints) + } + + private func createBaseConstraints(contentLayoutGuide: UILayoutGuide, frameLayoutGuide: UILayoutGuide) -> [NSLayoutConstraint] { + return [ + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.bottomAnchor.constraint(equalTo: visitHubButton.topAnchor, constant: -LayoutConstants.standardSpacing), + + contentView.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor), + contentView.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor), + contentView.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor), + contentView.widthAnchor.constraint(equalTo: frameLayoutGuide.widthAnchor), + + logoImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: LayoutConstants.largeSpacing), + logoImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + logoImageView.leadingAnchor.constraint(greaterThanOrEqualTo: contentView.readableContentGuide.leadingAnchor), + logoImageView.trailingAnchor.constraint(lessThanOrEqualTo: contentView.readableContentGuide.trailingAnchor), + logoImageView.heightAnchor.constraint(equalToConstant: LayoutConstants.logoHeight), + + hubImageView.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: LayoutConstants.largeSpacing), + hubImageView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + hubImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + hubImageView.heightAnchor.constraint(equalTo: hubImageView.widthAnchor, multiplier: LayoutConstants.hubImageMultiplier), + + titleLabel.topAnchor.constraint(equalTo: hubImageView.bottomAnchor, constant: LayoutConstants.titleSpacing), + titleLabel.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + + visitHubButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: LayoutConstants.standardSpacing), + visitHubButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -LayoutConstants.standardSpacing), + visitHubButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -LayoutConstants.standardSpacing), + visitHubButton.heightAnchor.constraint(equalToConstant: LayoutConstants.buttonHeight) + ] + } + + private func addOptionalSubtitleConstraints(to constraints: inout [NSLayoutConstraint]) -> NSLayoutYAxisAnchor { + guard viewModel.headerSubtitle != nil else { + return titleLabel.bottomAnchor + } + + constraints.append(contentsOf: [ + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: LayoutConstants.titleToSubtitleSpacing), + subtitleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding), + subtitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -LayoutConstants.horizontalPadding) + ]) + return subtitleLabel.bottomAnchor + } + + private func addContentConstraints(to constraints: inout [NSLayoutConstraint], topAnchor: NSLayoutYAxisAnchor) -> UIView { + if viewModel.hubSteps != nil { + constraints.append(contentsOf: [ + hubStepsStackView.topAnchor.constraint(equalTo: topAnchor, constant: LayoutConstants.subtitleToStepsSpacing), + hubStepsStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding), + hubStepsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -LayoutConstants.horizontalPadding) + ]) + return hubStepsStackView + } else { + constraints.append(contentsOf: [ + featuresLabel.topAnchor.constraint(equalTo: topAnchor, constant: LayoutConstants.titleToFeaturesSpacing), + featuresLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding), + featuresLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -LayoutConstants.horizontalPadding) + ]) + return featuresLabel + } + } + + private func addFooterConstraints(to constraints: inout [NSLayoutConstraint], lastContentView: UIView) { + if viewModel.footerText != nil { + constraints.append(contentsOf: [ + footerTextView.topAnchor.constraint(equalTo: lastContentView.bottomAnchor, constant: LayoutConstants.largeSpacing), + footerTextView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: LayoutConstants.horizontalPadding), + footerTextView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -LayoutConstants.horizontalPadding), + footerTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -LayoutConstants.titleSpacing) + ]) + } else { + constraints.append( + lastContentView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -LayoutConstants.titleSpacing) + ) + } + } + + private func createFooterAttributedText() -> NSAttributedString { + guard let footerText = viewModel.footerText, + let docsButtonTitle = viewModel.docsButtonTitle, + let docsURL = viewModel.docsURL else { + return NSAttributedString() + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + paragraphStyle.lineBreakMode = .byWordWrapping + + let font = UIFont.preferredFont(forTextStyle: .footnote) + let textAttributes: [NSAttributedString.Key: Any] = [ + .foregroundColor: UIColor.secondaryLabel, + .paragraphStyle: paragraphStyle, + .font: font + ] + let linkAttributes: [NSAttributedString.Key: Any] = [ + .link: docsURL, + .paragraphStyle: paragraphStyle, + .font: font + ] + + let text = NSMutableAttributedString(string: footerText, attributes: textAttributes) + text.append(NSAttributedString(string: "\u{00A0}", attributes: textAttributes)) + text.append(NSAttributedString(string: docsButtonTitle, attributes: linkAttributes)) + text.append(NSAttributedString(string: ".", attributes: textAttributes)) + + return text + } + + @objc private func visitHubButtonTapped() { + guard let url = viewModel.forTeamsURL else { + return + } + UIApplication.shared.open(url) + } +} diff --git a/Cryptomator/VaultDetail/ShareVault/ShareVaultViewModel.swift b/Cryptomator/VaultDetail/ShareVault/ShareVaultViewModel.swift new file mode 100644 index 000000000..d3a60e4dd --- /dev/null +++ b/Cryptomator/VaultDetail/ShareVault/ShareVaultViewModel.swift @@ -0,0 +1,71 @@ +// +// ShareVaultViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 24.10.25. +// Copyright © 2025 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import Foundation + +enum ShareVaultType { + case normal + case hub(URL) +} + +protocol ShareVaultViewModelProtocol: AnyObject { + var title: String { get } + var logoImageName: String { get } + var headerTitle: String { get } + var headerSubtitle: String? { get } + var featuresText: String? { get } + var hubSteps: [(String, String)]? { get } + var footerText: String? { get } + var docsButtonTitle: String? { get } + var docsURL: URL? { get } + var forTeamsButtonTitle: String { get } + var forTeamsURL: URL? { get } +} + +class ShareVaultViewModel: ShareVaultViewModelProtocol { + let title = LocalizedString.getValue("shareVault.title") + let logoImageName = "cryptomator-hub-logo" + let headerTitle: String + let headerSubtitle: String? + let featuresText: String? + let hubSteps: [(String, String)]? + let footerText: String? + let docsButtonTitle: String? + let docsURL: URL? + let forTeamsButtonTitle: String + let forTeamsURL: URL? + + init(type: ShareVaultType) { + switch type { + case .normal: + self.headerTitle = LocalizedString.getValue("shareVault.header.title") + self.headerSubtitle = nil + self.featuresText = LocalizedString.getValue("shareVault.header.features") + self.hubSteps = nil + self.footerText = LocalizedString.getValue("shareVault.footer.text") + self.docsButtonTitle = LocalizedString.getValue("shareVault.footer.link") + self.docsURL = URL(string: "https://docs.cryptomator.org/security/best-practices/#sharing-of-vaults") + self.forTeamsButtonTitle = LocalizedString.getValue("shareVault.button.visitHub") + self.forTeamsURL = URL(string: "https://cryptomator.org/for-teams/") + case let .hub(hubURL): + self.headerTitle = LocalizedString.getValue("shareVault.hub.header.title") + self.headerSubtitle = LocalizedString.getValue("shareVault.hub.header.subtitle") + self.featuresText = nil + self.hubSteps = [ + ("1.circle.fill", LocalizedString.getValue("shareVault.hub.step1")), + ("2.circle.fill", LocalizedString.getValue("shareVault.hub.step2")) + ] + self.footerText = nil + self.docsButtonTitle = nil + self.docsURL = nil + self.forTeamsButtonTitle = LocalizedString.getValue("shareVault.hub.button.openHub") + self.forTeamsURL = hubURL + } + } +} diff --git a/Cryptomator/VaultDetail/VaultDetailCoordinator.swift b/Cryptomator/VaultDetail/VaultDetailCoordinator.swift index bc03c10e9..42fb3c2b3 100644 --- a/Cryptomator/VaultDetail/VaultDetailCoordinator.swift +++ b/Cryptomator/VaultDetail/VaultDetailCoordinator.swift @@ -97,6 +97,13 @@ class VaultDetailCoordinator: Coordinator { changePasswordViewController.coordinator = self navigationController.pushViewController(changePasswordViewController, animated: true) } + + func showShareVault() { + let child = ShareVaultCoordinator(vaultInfo: vaultInfo, navigationController: navigationController) + child.parentCoordinator = self + childCoordinators.append(child) + child.start() + } } extension VaultDetailCoordinator: VaultNaming { diff --git a/Cryptomator/VaultDetail/VaultDetailViewController.swift b/Cryptomator/VaultDetail/VaultDetailViewController.swift index d957c8397..bda83927a 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewController.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewController.swift @@ -79,6 +79,8 @@ class VaultDetailViewController: BaseUITableViewController { coordinator?.changeVaultPassword() case let .showKeepUnlockedScreen(currentKeepUnlockedDuration): coordinator?.showKeepUnlockedSettings(currentKeepUnlockedDuration: currentKeepUnlockedDuration) + case .shareVault: + coordinator?.showShareVault() } } diff --git a/Cryptomator/VaultDetail/VaultDetailViewModel.swift b/Cryptomator/VaultDetail/VaultDetailViewModel.swift index f493c5a1c..25731ba88 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewModel.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewModel.swift @@ -42,11 +42,13 @@ enum VaultDetailButtonAction { case showMoveVault case showChangeVaultPassword case showKeepUnlockedScreen(currentKeepUnlockedDuration: Bindable) + case shareVault } private enum VaultDetailSection { case vaultInfoSection case lockingSection + case shareVaultSection case removeVaultSection case moveVaultSection case changeVaultPasswordSection @@ -85,7 +87,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private var subscribers = Set() private lazy var sections: [VaultDetailSection] = { - var sections: [VaultDetailSection] = [.vaultInfoSection, .lockingSection] + var sections: [VaultDetailSection] = [.vaultInfoSection, .lockingSection, .shareVaultSection] if vaultIsEligibleToMove() { sections.append(.moveVaultSection) } @@ -110,6 +112,9 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { ButtonCellViewModel(action: .openVaultInFilesApp, title: LocalizedString.getValue("common.cells.openInFilesApp")) ], .lockingSection: lockSectionCells, + .shareVaultSection: [ + ButtonCellViewModel(action: .shareVault, title: LocalizedString.getValue("vaultDetail.button.shareVault"), titleTextColor: .label) + ], .moveVaultSection: vaultIsEligibleToMove() ? [ renameVaultCellViewModel, moveVaultCellViewModel @@ -144,6 +149,7 @@ class VaultDetailViewModel: VaultDetailViewModelProtocol { private lazy var sectionFooter: [VaultDetailSection: HeaderFooterViewModel] = [.vaultInfoSection: VaultDetailInfoFooterViewModel(vault: vaultInfo), .changeVaultPasswordSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.changePassword.footer")), .lockingSection: unlockSectionFooterViewModel, + .shareVaultSection: BaseHeaderFooterViewModel(title: LocalizedString.getValue("vaultDetail.shareVault.footer")), .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, vaultInfo: vaultInfo) diff --git a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift index 440cc8355..b874113a8 100644 --- a/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift +++ b/CryptomatorCommon/Sources/CryptomatorCommonCore/Hub/CryptomatorHubAuthenticator.swift @@ -365,7 +365,7 @@ extension String { } } -extension HubConfig { +public extension HubConfig { func getAPIBaseURL() -> URL? { if let apiBaseUrl { return URL(string: apiBaseUrl) diff --git a/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/Contents.json b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/Contents.json new file mode 100644 index 000000000..aba0726cd --- /dev/null +++ b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "cryptomator-hub-logo@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cryptomator-hub-logo@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cryptomator-hub-logo@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@1x.png b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@1x.png new file mode 100644 index 000000000..32d2d6308 Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@1x.png differ diff --git a/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@2x.png b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@2x.png new file mode 100644 index 000000000..bb3d2e703 Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@2x.png differ diff --git a/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@3x.png b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@3x.png new file mode 100644 index 000000000..43b7f1270 Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub-logo.imageset/cryptomator-hub-logo@3x.png differ diff --git a/SharedResources/Assets.xcassets/cryptomator-hub.imageset/Contents.json b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/Contents.json new file mode 100644 index 000000000..53e96a0d3 --- /dev/null +++ b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "cryptomator-hub@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cryptomator-hub@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "cryptomator-hub@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@1x.png b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@1x.png new file mode 100644 index 000000000..de0a5828f Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@1x.png differ diff --git a/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@2x.png b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@2x.png new file mode 100644 index 000000000..311adce69 Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@2x.png differ diff --git a/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@3x.png b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@3x.png new file mode 100644 index 000000000..9a3cd19ee Binary files /dev/null and b/SharedResources/Assets.xcassets/cryptomator-hub.imageset/cryptomator-hub@3x.png differ diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index ea432fa88..847c19828 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -282,7 +282,21 @@ "vaultDetail.button.moveVault" = "Move"; "vaultDetail.button.removeVault" = "Remove from Vault List"; "vaultDetail.button.renameVault" = "Rename"; +"vaultDetail.button.shareVault" = "Share Vault"; +"vaultDetail.shareVault.footer" = "Securely collaborate with your team using Cryptomator Hub."; "vaultDetail.changePassword.footer" = "Select a strong password for your vault that only you know and keep it in a safe place."; + +"shareVault.title" = "Share Vault"; +"shareVault.header.title" = "The secure way to work in teams"; +"shareVault.header.features" = "Collaborate securely with your team. Grant and revoke access anytime without sharing passwords."; +"shareVault.footer.text" = "For more information, check out the best practices suggestions in our"; +"shareVault.footer.link" = "docs"; +"shareVault.button.visitHub" = "Visit Cryptomator Hub"; +"shareVault.hub.header.title" = "How to share a Hub vault"; +"shareVault.hub.header.subtitle" = "In order to share the vault content with another team member, you have to perform two steps:"; +"shareVault.hub.step1" = "Share access of the encrypted vault folder via cloud storage."; +"shareVault.hub.step2" = "Grant access to team member in Cryptomator Hub."; +"shareVault.hub.button.openHub" = "Open Hub"; "vaultDetail.disabledBiometricalUnlock.footer" = "If you enable %@, your vault password will be stored in the iOS keychain."; "vaultDetail.enabledBiometricalUnlock.footer" = "Your vault password will only be required if %@ authentication fails."; "vaultDetail.info.footer.accessVault" = "Access the vault via the Files app.";