Skip to content
Merged
24 changes: 24 additions & 0 deletions Cryptomator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,16 @@
74F5DC1C26DCD2FB00AFE989 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */; };
74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; };
74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; };
988E23D6223540118B002237 /* ShareVaultCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988E23D6223540118B002238 /* ShareVaultCoordinator.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 /* SharePointAuthenticating.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53292D142B9200F30FE9 /* SharePointAuthenticating.swift */; };
B3544E282EC1F564006B7BA9 /* ShareVaultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3544E272EC1F564006B7BA9 /* ShareVaultView.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 */; };
B3D19A442CB937C700CD18A5 /* FileProviderCoordinatorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -1058,12 +1062,16 @@
74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = "<group>"; };
74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = "<group>"; };
988E23D6223540118B002238 /* ShareVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultCoordinator.swift; sourceTree = "<group>"; };
B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = "<group>"; };
B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = "<group>"; };
B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = "<group>"; };
B34C53292D142B9200F30FE9 /* SharePointAuthenticating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointAuthenticating.swift; sourceTree = "<group>"; };
B3544E272EC1F564006B7BA9 /* ShareVaultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultView.swift; sourceTree = "<group>"; };
B379DBBE2D27F58C003B5849 /* SharePointDriveListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewController.swift; sourceTree = "<group>"; };
B379DBC02D27F5A4003B5849 /* SharePointDriveListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharePointDriveListViewModel.swift; sourceTree = "<group>"; };
B3C397FC2EB10FC0001280AC /* ShareVaultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultViewController.swift; sourceTree = "<group>"; };
B3C397FF2EB110F9001280AC /* ShareVaultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultViewModel.swift; sourceTree = "<group>"; };
B3D19A432CB937BF00CD18A5 /* FileProviderCoordinatorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileProviderCoordinatorError.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -1282,6 +1290,7 @@
4A4B7E3E26B2ABC0009BFDB1 /* VaultDetailViewController.swift */,
4A4B7E4126B2AD6F009BFDB1 /* VaultDetailViewModel.swift */,
4AB8539B26BA8A8200555F00 /* VaultPasswordVerifying.swift */,
B3C397FD2EB10FC0001280AC /* ShareVault */,
4AF45357271F2A8B00CF1919 /* RenameVault */,
4A0337C82726FBEC001753B7 /* MoveVault */,
4A91D8CF272ADC7E003F8BD8 /* ChangePassword */,
Expand Down Expand Up @@ -2114,6 +2123,17 @@
path = Purchase;
sourceTree = "<group>";
};
B3C397FD2EB10FC0001280AC /* ShareVault */ = {
isa = PBXGroup;
children = (
B3544E272EC1F564006B7BA9 /* ShareVaultView.swift */,
988E23D6223540118B002238 /* ShareVaultCoordinator.swift */,
B3C397FF2EB110F9001280AC /* ShareVaultViewModel.swift */,
B3C397FC2EB10FC0001280AC /* ShareVaultViewController.swift */,
);
path = ShareVault;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -2783,6 +2803,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 */,
Expand Down Expand Up @@ -2810,6 +2832,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 */,
Expand All @@ -2824,6 +2847,7 @@
4A4B7E7426B954D2009BFDB1 /* HeaderFooterViewModel.swift in Sources */,
4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */,
74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */,
B3544E282EC1F564006B7BA9 /* ShareVaultView.swift in Sources */,
B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */,
4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */,
4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */,
Expand Down
62 changes: 62 additions & 0 deletions Cryptomator/VaultDetail/ShareVault/ShareVaultCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// 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 {
guard let hubURL = extractHubVaultURL() else {
showHubURLExtractionError()
return
}
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 showHubURLExtractionError() {
let alert = UIAlertController(
title: LocalizedString.getValue("shareVault.error.hubURLExtraction.title"),
message: LocalizedString.getValue("shareVault.error.hubURLExtraction.message"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: LocalizedString.getValue("common.button.ok"), style: .default))
navigationController.present(alert, 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()
}
}
129 changes: 129 additions & 0 deletions Cryptomator/VaultDetail/ShareVault/ShareVaultView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// ShareVaultView.swift
// Cryptomator
//
// Created by Majid Achhoud on 24.10.25.
// Copyright © 2025 Skymatic GmbH. All rights reserved.
//

import CryptomatorCommonCore
import SwiftUI
import UIKit

struct ShareVaultView: View {
@ObservedObject var viewModel: ShareVaultViewModel

var body: some View {
ZStack {
Color.cryptomatorBackground
.ignoresSafeArea()

VStack {
ScrollView {
VStack(spacing: 0) {
Image(viewModel.logoImageName)
.resizable()
.scaledToFit()
.frame(height: 44)
.padding(.top, 32)

Image("cryptomator-hub")
.resizable()
.scaledToFit()
.aspectRatio(1 / 0.7, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding(.top, 32)
.padding(.horizontal)

Text(viewModel.headerTitle)
.font(.title3)
.multilineTextAlignment(.center)
.padding(.top, 24)
.padding(.horizontal, 32)

if let subtitle = viewModel.headerSubtitle {
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 16)
.padding(.horizontal, 32)
}

if let features = viewModel.featuresText {
Text(features)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.top, 8)
.padding(.horizontal, 32)
}

if let steps = viewModel.hubSteps {
VStack(spacing: 16) {
ForEach(Array(steps.enumerated()), id: \.offset) { _, step in
HStack(alignment: .top, spacing: 12) {
Image(systemName: step.0)
.foregroundColor(.cryptomatorPrimary)
.frame(width: 24, height: 24)

Text(step.1)
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding(.top, 20)
.padding(.horizontal, 32)
}

if let footerText = viewModel.footerText,
let docsURL = viewModel.docsURL {
VStack(spacing: 0) {
Text(LocalizedStringKey(String(format: footerText, docsURL.absoluteString)))
.font(.footnote)
.foregroundColor(.secondary)
.accentColor(.cryptomatorPrimary)
.multilineTextAlignment(.center)
}
.padding(.top, 32)
.padding(.horizontal, 32)
.padding(.bottom, 24)
} else {
Spacer()
.frame(height: 24)
}
}
}

if let url = viewModel.hubURL {
Button(
action: {
UIApplication.shared.open(url)
},
label: {
Text(viewModel.hubButtonTitle)
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background(Color.cryptomatorPrimary)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
)
.padding(.horizontal, 16)
.padding(.bottom, 16)
}
}
}
}
}

struct ShareVaultView_Previews: PreviewProvider {
static var previews: some View {
ShareVaultView(viewModel: ShareVaultViewModel(type: .normal))
}
}
47 changes: 47 additions & 0 deletions Cryptomator/VaultDetail/ShareVault/ShareVaultViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ShareVaultViewController.swift
// Cryptomator
//
// Created by Majid Achhoud on 24.10.25.
// Copyright © 2025 Skymatic GmbH. All rights reserved.
//

import CryptomatorCommonCore
import SwiftUI
import UIKit

/**
ViewController for the `ShareVaultView`.

This ViewController builds the bridge between UIKit and the SwiftUI `ShareVaultView`.
This bridge is needed to integrate the SwiftUI view within a UIKit `UINavigationController`.
*/
class ShareVaultViewController: UIViewController {
weak var coordinator: ShareVaultCoordinator?
private let viewModel: ShareVaultViewModel

init(viewModel: ShareVaultViewModel) {
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
setupSwiftUIView()
}

private func setupSwiftUIView() {
let child = UIHostingController(rootView: ShareVaultView(viewModel: viewModel))
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate(child.view.constraints(equalTo: view))
}
}
67 changes: 67 additions & 0 deletions Cryptomator/VaultDetail/ShareVault/ShareVaultViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// 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 docsURL: URL? { get }
var hubButtonTitle: String { get }
var hubURL: URL? { get }
}

class ShareVaultViewModel: ShareVaultViewModelProtocol, ObservableObject {
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 docsURL: URL?
let hubButtonTitle: String
let hubURL: URL?

init(type: ShareVaultType) {
switch type {
case .normal:
self.headerTitle = LocalizedString.getValue("shareVault.normal.header.title")
self.headerSubtitle = nil
self.featuresText = LocalizedString.getValue("shareVault.normal.header.features")
self.hubSteps = nil
self.footerText = LocalizedString.getValue("shareVault.normal.footer.text")
self.docsURL = URL(string: "https://docs.cryptomator.org/security/best-practices/#sharing-of-vaults")
self.hubButtonTitle = LocalizedString.getValue("shareVault.normal.button.visitHub")
self.hubURL = 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.docsURL = nil
self.hubButtonTitle = LocalizedString.getValue("shareVault.hub.button.openHub")
self.hubURL = hubURL
}
}
}
Loading
Loading