From 37030755ace8489e5db8175cc4189522310b4d40 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Thu, 6 Nov 2025 09:34:13 +0100 Subject: [PATCH 1/9] Show purchase status and restore option in settings --- Cryptomator/Settings/SettingsViewModel.swift | 17 +++++++++++++---- SharedResources/en.lproj/Localizable.strings | 2 ++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index d761538c1..f4a8e81bd 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -71,12 +71,21 @@ class SettingsViewModel: TableViewModel { } private var aboutSectionElements: [TableViewCellViewModel] { - var elements = [ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator"))] + var elements: [TableViewCellViewModel] = [ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator"))] + if cryptomatorSettings.hasRunningSubscription { - elements.append(.init(action: .showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions"))) - elements.append(.init(action: .restorePurchase, title: LocalizedString.getValue("purchase.restorePurchase.button"))) - } else if !cryptomatorSettings.fullVersionUnlocked { + elements.append(ButtonCellViewModel(action: .showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions"))) + elements.append(ButtonCellViewModel(action: .restorePurchase, title: LocalizedString.getValue("purchase.restorePurchase.button"))) + } else if cryptomatorSettings.fullVersionUnlocked { + let statusCell = BindableTableViewCellViewModel( + title: LocalizedString.getValue("settings.fullVersionStatus"), + selectionStyle: .none, + accessoryType: .checkmark + ) + elements.append(statusCell) + } else { elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion"))) + elements.append(ButtonCellViewModel(action: .restorePurchase, title: LocalizedString.getValue("purchase.restorePurchase.button"))) } return elements } diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index ea432fa88..e8758e675 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -213,6 +213,8 @@ "settings.sendLogFile" = "Send Log File"; "settings.shortcutsGuide" = "Shortcuts Guide"; "settings.unlockFullVersion" = "Unlock Full Version"; +"settings.fullVersionStatus" = "Full Version"; +"settings.fullVersionStatus.footer" = "You're using Cryptomator Full Version. Thank you for your support!"; "sharePoint.enterURL.title" = "Enter SharePoint URL"; "sharePoint.enterURL.placeholder" = "SharePoint Site URL"; From 22a98a55dcd7d5a77cf1480d78846893bd2c4a96 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Tue, 11 Nov 2025 10:31:50 +0100 Subject: [PATCH 2/9] Remove restore button for subscription users --- Cryptomator/Settings/SettingsViewModel.swift | 1 - SharedResources/en.lproj/Localizable.strings | 1 - 2 files changed, 2 deletions(-) diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index f4a8e81bd..eb3431025 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -75,7 +75,6 @@ class SettingsViewModel: TableViewModel { if cryptomatorSettings.hasRunningSubscription { elements.append(ButtonCellViewModel(action: .showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions"))) - elements.append(ButtonCellViewModel(action: .restorePurchase, title: LocalizedString.getValue("purchase.restorePurchase.button"))) } else if cryptomatorSettings.fullVersionUnlocked { let statusCell = BindableTableViewCellViewModel( title: LocalizedString.getValue("settings.fullVersionStatus"), diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index e8758e675..ea1998962 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -214,7 +214,6 @@ "settings.shortcutsGuide" = "Shortcuts Guide"; "settings.unlockFullVersion" = "Unlock Full Version"; "settings.fullVersionStatus" = "Full Version"; -"settings.fullVersionStatus.footer" = "You're using Cryptomator Full Version. Thank you for your support!"; "sharePoint.enterURL.title" = "Enter SharePoint URL"; "sharePoint.enterURL.placeholder" = "SharePoint Site URL"; From b295ee2627c9c65e4b575fc3ec976b36d61918d4 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Wed, 12 Nov 2025 09:06:07 +0100 Subject: [PATCH 3/9] Remove restore purchase button from settings --- Cryptomator/Settings/SettingsViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index eb3431025..6829439cf 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -84,7 +84,6 @@ class SettingsViewModel: TableViewModel { elements.append(statusCell) } else { elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion"))) - elements.append(ButtonCellViewModel(action: .restorePurchase, title: LocalizedString.getValue("purchase.restorePurchase.button"))) } return elements } From e94263dd5da1a55d3a2a9a6f8c7f15ba7eb61459 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Fri, 21 Nov 2025 15:53:48 +0100 Subject: [PATCH 4/9] Improve purchase status display in settings --- Cryptomator.xcodeproj/project.pbxproj | 8 ++ Cryptomator/Settings/PurchaseStatusCell.swift | 76 +++++++++++++++++++ .../PurchaseStatusCellViewModel.swift | 24 ++++++ .../Settings/SettingsViewController.swift | 5 ++ Cryptomator/Settings/SettingsViewModel.swift | 55 ++++++++++---- SharedResources/en.lproj/Localizable.strings | 4 +- 6 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 Cryptomator/Settings/PurchaseStatusCell.swift create mode 100644 Cryptomator/Settings/PurchaseStatusCellViewModel.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 1b1d0ed08..d0a9247ce 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -439,6 +439,8 @@ 74F5DC1C26DCD2FB00AFE989 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */; }; 74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; }; 74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; }; + B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */; }; + B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D12ED0778800E82B07 /* PurchaseStatusCell.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 */; }; @@ -1058,6 +1060,8 @@ 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = ""; }; 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; + B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCell.swift; sourceTree = ""; }; + B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCellViewModel.swift; sourceTree = ""; }; B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = ""; }; B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = ""; }; B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = ""; }; @@ -2037,6 +2041,8 @@ 740D3683266A1B180058744D /* SettingsCoordinator.swift */, 740D367D266A18DF0058744D /* SettingsViewController.swift */, 740D3681266A19150058744D /* SettingsViewModel.swift */, + B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */, + B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */, 7408E6CB26779BC200D7FAEA /* About */, ); path = Settings; @@ -2783,6 +2789,8 @@ 4A644B57267C958F008CBB9A /* ChildCoordinator.swift in Sources */, 4A53CC15267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift in Sources */, 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */, + B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */, + B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */, 4A136132276770BB0077EB7F /* SnapshotVaultListViewModel.swift in Sources */, 4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */, 747C35172762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift in Sources */, diff --git a/Cryptomator/Settings/PurchaseStatusCell.swift b/Cryptomator/Settings/PurchaseStatusCell.swift new file mode 100644 index 000000000..3b70f756f --- /dev/null +++ b/Cryptomator/Settings/PurchaseStatusCell.swift @@ -0,0 +1,76 @@ +// +// PurchaseStatusCell.swift +// Cryptomator +// +// Created by Majid Achhoud on 20.11.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import Combine +import CryptomatorCommonCore +import UIKit + +class PurchaseStatusCell: UITableViewCell, ConfigurableTableViewCell { + private let iconImageView = UIImageView() + private let titleLabel = UILabel() + private let subtitleLabel = UILabel() + lazy var subscribers = Set() + + func configure(with viewModel: TableViewCellViewModel) { + guard let viewModel = viewModel as? PurchaseStatusCellViewModel else { + return + } + iconImageView.image = UIImage(systemName: viewModel.iconName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 22)) + viewModel.title.$value.assign(to: \.text, on: titleLabel).store(in: &subscribers) + viewModel.subtitle.$value.assign(to: \.text, on: subtitleLabel).store(in: &subscribers) + accessoryType = .disclosureIndicator + selectionStyle = .default + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + iconImageView.translatesAutoresizingMaskIntoConstraints = false + iconImageView.contentMode = .scaleAspectFit + iconImageView.tintColor = .cryptomatorPrimary + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = .preferredFont(forTextStyle: .body) + titleLabel.textColor = .label + titleLabel.numberOfLines = 0 + + subtitleLabel.translatesAutoresizingMaskIntoConstraints = false + subtitleLabel.font = .preferredFont(forTextStyle: .footnote) + subtitleLabel.textColor = .secondaryLabel + subtitleLabel.numberOfLines = 0 + + contentView.addSubview(iconImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(subtitleLabel) + + NSLayoutConstraint.activate([ + iconImageView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: 29), + iconImageView.heightAnchor.constraint(equalToConstant: 29), + + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), + titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 12), + titleLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), + + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + subtitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -11) + ]) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func removeAllBindings() { + subscribers.forEach { $0.cancel() } + } +} diff --git a/Cryptomator/Settings/PurchaseStatusCellViewModel.swift b/Cryptomator/Settings/PurchaseStatusCellViewModel.swift new file mode 100644 index 000000000..a737a7d4d --- /dev/null +++ b/Cryptomator/Settings/PurchaseStatusCellViewModel.swift @@ -0,0 +1,24 @@ +// +// PurchaseStatusCellViewModel.swift +// Cryptomator +// +// Created by Majid Achhoud on 20.11.24. +// Copyright © 2024 Skymatic GmbH. All rights reserved. +// + +import CryptomatorCommonCore +import UIKit + +class PurchaseStatusCellViewModel: TableViewCellViewModel { + override var type: ConfigurableTableViewCell.Type { PurchaseStatusCell.self } + + let iconName: String + let title: Bindable + let subtitle: Bindable + + init(iconName: String, title: String, subtitle: String) { + self.iconName = iconName + self.title = Bindable(title) + self.subtitle = Bindable(subtitle) + } +} diff --git a/Cryptomator/Settings/SettingsViewController.swift b/Cryptomator/Settings/SettingsViewController.swift index 151850e53..b98f5b33e 100644 --- a/Cryptomator/Settings/SettingsViewController.swift +++ b/Cryptomator/Settings/SettingsViewController.swift @@ -134,6 +134,11 @@ class SettingsViewController: StaticUITableViewController { case .none: break } + + if dataSource?.itemIdentifier(for: indexPath) is PurchaseStatusCellViewModel { + tableView.deselectRow(at: indexPath, animated: true) + coordinator?.showUnlockFullVersion() + } } private func refreshRows() { diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 6829439cf..7dc48d64d 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -28,7 +28,8 @@ enum SettingsButtonAction: String { } enum SettingsSection: Int { - case cloudServiceSection = 0 + case purchaseStatusSection = 0 + case cloudServiceSection case cacheSection case aboutSection case debugSection @@ -49,7 +50,11 @@ class SettingsViewModel: TableViewModel { } private var _sections: [Section] { - return [ + var sections: [Section] = [] + if !hasFullAccess { + sections.append(Section(id: .purchaseStatusSection, elements: [purchaseStatusCellViewModel])) + } + sections.append(contentsOf: [ Section(id: .cloudServiceSection, elements: [ ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showCloudServices, title: LocalizedString.getValue("settings.cloudServices")) ]), @@ -67,23 +72,25 @@ class SettingsViewModel: TableViewModel { ButtonCellViewModel(action: SettingsButtonAction.showContact, title: LocalizedString.getValue("settings.contact")), ButtonCellViewModel(action: SettingsButtonAction.showRateApp, title: LocalizedString.getValue("settings.rateApp")) ]) - ] + ]) + return sections } - private var aboutSectionElements: [TableViewCellViewModel] { - var elements: [TableViewCellViewModel] = [ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator"))] + override func getFooterTitle(for section: Int) -> String? { + guard sections[section].id == .aboutSection, hasFullAccess else { return nil } + return LocalizedString.getValue("settings.fullVersion.footer") + } + private var hasFullAccess: Bool { + cryptomatorSettings.hasRunningSubscription || cryptomatorSettings.fullVersionUnlocked + } + + private var aboutSectionElements: [TableViewCellViewModel] { + var elements: [TableViewCellViewModel] = [ + ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator")) + ] if cryptomatorSettings.hasRunningSubscription { - elements.append(ButtonCellViewModel(action: .showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions"))) - } else if cryptomatorSettings.fullVersionUnlocked { - let statusCell = BindableTableViewCellViewModel( - title: LocalizedString.getValue("settings.fullVersionStatus"), - selectionStyle: .none, - accessoryType: .checkmark - ) - elements.append(statusCell) - } else { - elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion"))) + elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions"))) } return elements } @@ -92,6 +99,24 @@ class SettingsViewModel: TableViewModel { private let clearCacheButtonCellViewModel = ButtonCellViewModel(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false) private var cryptomatorSettings: CryptomatorSettings + + private var purchaseStatusCellViewModel: PurchaseStatusCellViewModel { + let subtitle: String + if let trialExpirationDate = cryptomatorSettings.trialExpirationDate, trialExpirationDate > Date() { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + subtitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate)) + } else { + subtitle = LocalizedString.getValue("settings.freeTier.subtitle") + } + return PurchaseStatusCellViewModel( + iconName: "checkmark.seal.fill", + title: LocalizedString.getValue("settings.unlockFullVersion"), + subtitle: subtitle + ) + } + private lazy var debugModeViewModel: SwitchCellViewModel = { let viewModel = SwitchCellViewModel(title: LocalizedString.getValue("settings.debugMode"), isOn: cryptomatorSettings.debugModeEnabled) bindDebugModeViewModel(viewModel) diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index ea1998962..d4ce04afb 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -213,7 +213,9 @@ "settings.sendLogFile" = "Send Log File"; "settings.shortcutsGuide" = "Shortcuts Guide"; "settings.unlockFullVersion" = "Unlock Full Version"; -"settings.fullVersionStatus" = "Full Version"; +"settings.fullVersion.footer" = "You have unlocked the full version and gained write access to your vaults."; +"settings.trial.expirationDate" = "Trial Expiration Date: %@"; +"settings.freeTier.subtitle" = "Gain write access to your vaults."; "sharePoint.enterURL.title" = "Enter SharePoint URL"; "sharePoint.enterURL.placeholder" = "SharePoint Site URL"; From d6c38e6987ff88d9059233ed2f737a3bf8c822d9 Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Sat, 22 Nov 2025 15:04:32 +0100 Subject: [PATCH 5/9] Fix memory leak in PurchaseStatusCell --- Cryptomator/Settings/PurchaseStatusCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Cryptomator/Settings/PurchaseStatusCell.swift b/Cryptomator/Settings/PurchaseStatusCell.swift index 3b70f756f..1ce04ac6c 100644 --- a/Cryptomator/Settings/PurchaseStatusCell.swift +++ b/Cryptomator/Settings/PurchaseStatusCell.swift @@ -17,6 +17,7 @@ class PurchaseStatusCell: UITableViewCell, ConfigurableTableViewCell { lazy var subscribers = Set() func configure(with viewModel: TableViewCellViewModel) { + removeAllBindings() guard let viewModel = viewModel as? PurchaseStatusCellViewModel else { return } From 6e48f4353196639e3f417c6a7ae1c98ad99b938e Mon Sep 17 00:00:00 2001 From: Majid Achhoud Date: Mon, 24 Nov 2025 08:40:11 +0100 Subject: [PATCH 6/9] Add ShareVaultViewController to build target --- Cryptomator.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 0ee45747f..4f936308a 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -2855,6 +2855,7 @@ 4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */, 74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */, B3544E282EC1F564006B7BA9 /* ShareVaultView.swift in Sources */, + B3C397FE2EB10FC0001280AC /* ShareVaultViewController.swift in Sources */, B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */, 4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */, 4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */, From 7098fe5c3f9280c5041bc1e2300e78bc4a1a73a3 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 24 Nov 2025 15:34:56 +0100 Subject: [PATCH 7/9] Fix unnecessary diff [ci skip] --- Cryptomator.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 4f936308a..4c01ead56 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -2809,6 +2809,7 @@ 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 */, B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */, B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */, @@ -2855,7 +2856,6 @@ 4A5AC441275A5B3500342AA7 /* PurchaseAlert.swift in Sources */, 74C2BC5026E8FCC100BCAA03 /* PurchaseViewModel.swift in Sources */, B3544E282EC1F564006B7BA9 /* ShareVaultView.swift in Sources */, - B3C397FE2EB10FC0001280AC /* ShareVaultViewController.swift in Sources */, B379DBBF2D27F595003B5849 /* SharePointDriveListViewController.swift in Sources */, 4A644B53267BAFDA008CBB9A /* CreateNewFolderViewModel.swift in Sources */, 4AB8539826BA881F00555F00 /* VaultDetailUnlockVaultViewModel.swift in Sources */, From a24cc049298b1eb1c9a8f0e9f28b4c2bc2d332b1 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 24 Nov 2025 17:17:36 +0100 Subject: [PATCH 8/9] Replace PurchaseStatusCell with ButtonCellViewModel --- Cryptomator.xcodeproj/project.pbxproj | 8 -- ...stingVaultChooseFolderViewController.swift | 2 +- .../BindableTableViewCellViewModel.swift | 4 + .../Common/Cells/ButtonCellViewModel.swift | 18 ++--- .../Common/Cells/LoadingButtonCell.swift | 2 +- .../Common/Cells/SystemSymbolButtonCell.swift | 2 +- .../CreateNewFolderViewController.swift | 2 +- .../Common/StaticUITableViewController.swift | 2 +- Cryptomator/Settings/PurchaseStatusCell.swift | 77 ------------------- .../PurchaseStatusCellViewModel.swift | 24 ------ .../Settings/SettingsViewController.swift | 5 -- Cryptomator/Settings/SettingsViewModel.swift | 43 +++++------ .../VaultDetailViewController.swift | 2 +- 13 files changed, 37 insertions(+), 154 deletions(-) delete mode 100644 Cryptomator/Settings/PurchaseStatusCell.swift delete mode 100644 Cryptomator/Settings/PurchaseStatusCellViewModel.swift diff --git a/Cryptomator.xcodeproj/project.pbxproj b/Cryptomator.xcodeproj/project.pbxproj index 4c01ead56..f757672e3 100644 --- a/Cryptomator.xcodeproj/project.pbxproj +++ b/Cryptomator.xcodeproj/project.pbxproj @@ -440,8 +440,6 @@ 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 */; }; - B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */; }; - B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D12ED0778800E82B07 /* PurchaseStatusCell.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 */; }; @@ -1065,8 +1063,6 @@ 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = ""; }; 74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = ""; }; 988E23D6223540118B002238 /* ShareVaultCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVaultCoordinator.swift; sourceTree = ""; }; - B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCell.swift; sourceTree = ""; }; - B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCellViewModel.swift; sourceTree = ""; }; B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = ""; }; B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = ""; }; B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = ""; }; @@ -2050,8 +2046,6 @@ 740D3683266A1B180058744D /* SettingsCoordinator.swift */, 740D367D266A18DF0058744D /* SettingsViewController.swift */, 740D3681266A19150058744D /* SettingsViewModel.swift */, - B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */, - B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */, 7408E6CB26779BC200D7FAEA /* About */, ); path = Settings; @@ -2811,8 +2805,6 @@ 4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */, B3C397FE2EB10FC0001280AC /* ShareVaultViewController.swift in Sources */, 988E23D6223540118B002237 /* ShareVaultCoordinator.swift in Sources */, - B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */, - B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */, 4A136132276770BB0077EB7F /* SnapshotVaultListViewModel.swift in Sources */, 4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */, 747C35172762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift in Sources */, diff --git a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift index 35abf14f4..f8fbc947f 100644 --- a/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift +++ b/Cryptomator/AddVault/OpenExistingVault/OpenExistingVaultChooseFolderViewController.swift @@ -45,7 +45,7 @@ class OpenExistingVaultChooseFolderViewController: ChooseFolderViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if viewModel.foundMasterkey { - let cell = addVaultButtonViewModel.type.init() + let cell = addVaultButtonViewModel.type.init(style: addVaultButtonViewModel.cellStyle, reuseIdentifier: nil) cell.configure(with: addVaultButtonViewModel) return cell } else { diff --git a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift index 154850374..36e2c6f96 100644 --- a/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift +++ b/Cryptomator/Common/Cells/BindableTableViewCellViewModel.swift @@ -39,6 +39,10 @@ class TableViewCellViewModel: Hashable { fatalError("not implemented") } + var cellStyle: UITableViewCell.CellStyle { + return .default + } + private let identifier = UUID() static func == (lhs: TableViewCellViewModel, rhs: TableViewCellViewModel) -> Bool { diff --git a/Cryptomator/Common/Cells/ButtonCellViewModel.swift b/Cryptomator/Common/Cells/ButtonCellViewModel.swift index 5c0cfb32b..8e8a3b0ae 100644 --- a/Cryptomator/Common/Cells/ButtonCellViewModel.swift +++ b/Cryptomator/Common/Cells/ButtonCellViewModel.swift @@ -9,20 +9,16 @@ import UIKit class ButtonCellViewModel: BindableTableViewCellViewModel { - override var type: ConfigurableTableViewCell.Type { ButtonTableViewCell.self } + private let preferredCellStyle: UITableViewCell.CellStyle + override var cellStyle: UITableViewCell.CellStyle { preferredCellStyle } let action: T - init(action: T, title: String, titleTextColor: UIColor? = .cryptomatorPrimary, detailTitle: String? = nil, isEnabled: Bool = true, selectionStyle: UITableViewCell.SelectionStyle = .default, accessoryType: UITableViewCell.AccessoryType = .none) { + init(action: T, title: String, titleTextColor: UIColor? = .cryptomatorPrimary, detailTitle: String? = nil, image: UIImage? = nil, isEnabled: Bool = true, selectionStyle: UITableViewCell.SelectionStyle = .default, accessoryType: UITableViewCell.AccessoryType = .none, cellStyle: UITableViewCell.CellStyle = .value1) { self.action = action - super.init(title: title, titleTextColor: titleTextColor, detailTitle: detailTitle, isEnabled: isEnabled, selectionStyle: selectionStyle, accessoryType: accessoryType) + self.preferredCellStyle = cellStyle + super.init(title: title, titleTextColor: titleTextColor, detailTitle: detailTitle, image: image, isEnabled: isEnabled, selectionStyle: selectionStyle, accessoryType: accessoryType) } - static func createDisclosureButton(action: T, title: String, detailTitle: String? = nil, accessoryType: UITableViewCell.AccessoryType = .disclosureIndicator, isEnabled: Bool = true) -> ButtonCellViewModel { - return ButtonCellViewModel(action: action, title: title, titleTextColor: nil, detailTitle: detailTitle, isEnabled: isEnabled, accessoryType: accessoryType) - } -} - -class ButtonTableViewCell: TableViewCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .value1, reuseIdentifier: reuseIdentifier) + static func createDisclosureButton(action: T, title: String, detailTitle: String? = nil, image: UIImage? = nil, accessoryType: UITableViewCell.AccessoryType = .disclosureIndicator, isEnabled: Bool = true, cellStyle: UITableViewCell.CellStyle = .value1) -> ButtonCellViewModel { + return ButtonCellViewModel(action: action, title: title, titleTextColor: nil, detailTitle: detailTitle, image: image, isEnabled: isEnabled, accessoryType: accessoryType, cellStyle: cellStyle) } } diff --git a/Cryptomator/Common/Cells/LoadingButtonCell.swift b/Cryptomator/Common/Cells/LoadingButtonCell.swift index 8f216609a..742c84c4f 100644 --- a/Cryptomator/Common/Cells/LoadingButtonCell.swift +++ b/Cryptomator/Common/Cells/LoadingButtonCell.swift @@ -8,7 +8,7 @@ import UIKit -class LoadingButtonCell: ButtonTableViewCell { +class LoadingButtonCell: TableViewCell { private lazy var loadingIndicator: UIActivityIndicatorView = { let loadingIndicator = UIActivityIndicatorView(style: .medium) loadingIndicator.hidesWhenStopped = true diff --git a/Cryptomator/Common/Cells/SystemSymbolButtonCell.swift b/Cryptomator/Common/Cells/SystemSymbolButtonCell.swift index f27663ee2..d6e95352c 100644 --- a/Cryptomator/Common/Cells/SystemSymbolButtonCell.swift +++ b/Cryptomator/Common/Cells/SystemSymbolButtonCell.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -class SystemSymbolButtonCell: ButtonTableViewCell { +class SystemSymbolButtonCell: TableViewCell { override func configure(with viewModel: TableViewCellViewModel) { super.configure(with: viewModel) guard let viewModel = viewModel as? SystemSymbolNameProviding else { diff --git a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift index 545004134..0e7d67d88 100644 --- a/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift +++ b/Cryptomator/Common/ChooseFolder/CreateNewFolderViewController.swift @@ -35,7 +35,7 @@ class CreateNewFolderViewController: SingleSectionStaticUITableViewController { override func configureDataSource() { dataSource = BaseDiffableDataSource(viewModel: viewModel, tableView: tableView) { _, _, cellViewModel -> UITableViewCell? in - let cell = cellViewModel.type.init() + let cell = cellViewModel.type.init(style: cellViewModel.cellStyle, reuseIdentifier: nil) cell.configure(with: cellViewModel) if let textFieldCell = cell as? TextFieldCell { let imageConfiguration = UIImage.SymbolConfiguration(font: UIFont.preferredFont(forTextStyle: .title2)) diff --git a/Cryptomator/Common/StaticUITableViewController.swift b/Cryptomator/Common/StaticUITableViewController.swift index 49ca1c030..118b514b0 100644 --- a/Cryptomator/Common/StaticUITableViewController.swift +++ b/Cryptomator/Common/StaticUITableViewController.swift @@ -27,7 +27,7 @@ class StaticUITableViewController: BaseUITableViewControl func configureDataSource() { dataSource = BaseDiffableDataSource(viewModel: viewModel, tableView: tableView) { _, _, cellViewModel -> UITableViewCell? in - let cell = cellViewModel.type.init() + let cell = cellViewModel.type.init(style: cellViewModel.cellStyle, reuseIdentifier: nil) cell.configure(with: cellViewModel) return cell } diff --git a/Cryptomator/Settings/PurchaseStatusCell.swift b/Cryptomator/Settings/PurchaseStatusCell.swift deleted file mode 100644 index 1ce04ac6c..000000000 --- a/Cryptomator/Settings/PurchaseStatusCell.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// PurchaseStatusCell.swift -// Cryptomator -// -// Created by Majid Achhoud on 20.11.24. -// Copyright © 2024 Skymatic GmbH. All rights reserved. -// - -import Combine -import CryptomatorCommonCore -import UIKit - -class PurchaseStatusCell: UITableViewCell, ConfigurableTableViewCell { - private let iconImageView = UIImageView() - private let titleLabel = UILabel() - private let subtitleLabel = UILabel() - lazy var subscribers = Set() - - func configure(with viewModel: TableViewCellViewModel) { - removeAllBindings() - guard let viewModel = viewModel as? PurchaseStatusCellViewModel else { - return - } - iconImageView.image = UIImage(systemName: viewModel.iconName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 22)) - viewModel.title.$value.assign(to: \.text, on: titleLabel).store(in: &subscribers) - viewModel.subtitle.$value.assign(to: \.text, on: subtitleLabel).store(in: &subscribers) - accessoryType = .disclosureIndicator - selectionStyle = .default - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - iconImageView.translatesAutoresizingMaskIntoConstraints = false - iconImageView.contentMode = .scaleAspectFit - iconImageView.tintColor = .cryptomatorPrimary - - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.font = .preferredFont(forTextStyle: .body) - titleLabel.textColor = .label - titleLabel.numberOfLines = 0 - - subtitleLabel.translatesAutoresizingMaskIntoConstraints = false - subtitleLabel.font = .preferredFont(forTextStyle: .footnote) - subtitleLabel.textColor = .secondaryLabel - subtitleLabel.numberOfLines = 0 - - contentView.addSubview(iconImageView) - contentView.addSubview(titleLabel) - contentView.addSubview(subtitleLabel) - - NSLayoutConstraint.activate([ - iconImageView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), - iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImageView.widthAnchor.constraint(equalToConstant: 29), - iconImageView.heightAnchor.constraint(equalToConstant: 29), - - titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11), - titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 12), - titleLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), - - subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), - subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), - subtitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -11) - ]) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func removeAllBindings() { - subscribers.forEach { $0.cancel() } - } -} diff --git a/Cryptomator/Settings/PurchaseStatusCellViewModel.swift b/Cryptomator/Settings/PurchaseStatusCellViewModel.swift deleted file mode 100644 index a737a7d4d..000000000 --- a/Cryptomator/Settings/PurchaseStatusCellViewModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PurchaseStatusCellViewModel.swift -// Cryptomator -// -// Created by Majid Achhoud on 20.11.24. -// Copyright © 2024 Skymatic GmbH. All rights reserved. -// - -import CryptomatorCommonCore -import UIKit - -class PurchaseStatusCellViewModel: TableViewCellViewModel { - override var type: ConfigurableTableViewCell.Type { PurchaseStatusCell.self } - - let iconName: String - let title: Bindable - let subtitle: Bindable - - init(iconName: String, title: String, subtitle: String) { - self.iconName = iconName - self.title = Bindable(title) - self.subtitle = Bindable(subtitle) - } -} diff --git a/Cryptomator/Settings/SettingsViewController.swift b/Cryptomator/Settings/SettingsViewController.swift index b98f5b33e..151850e53 100644 --- a/Cryptomator/Settings/SettingsViewController.swift +++ b/Cryptomator/Settings/SettingsViewController.swift @@ -134,11 +134,6 @@ class SettingsViewController: StaticUITableViewController { case .none: break } - - if dataSource?.itemIdentifier(for: indexPath) is PurchaseStatusCellViewModel { - tableView.deselectRow(at: indexPath, animated: true) - coordinator?.showUnlockFullVersion() - } } private func refreshRows() { diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 7dc48d64d..7c524454e 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -45,10 +45,19 @@ class SettingsViewModel: TableViewModel { return _sections } + override func getFooterTitle(for section: Int) -> String? { + guard sections[section].id == .aboutSection, hasFullAccess else { return nil } + return LocalizedString.getValue("settings.fullVersion.footer") + } + var showDebugModeWarning: AnyPublisher { return showDebugModeWarningPublisher.eraseToAnyPublisher() } + private var hasFullAccess: Bool { + cryptomatorSettings.hasRunningSubscription || cryptomatorSettings.fullVersionUnlocked + } + private var _sections: [Section] { var sections: [Section] = [] if !hasFullAccess { @@ -76,15 +85,6 @@ class SettingsViewModel: TableViewModel { return sections } - override func getFooterTitle(for section: Int) -> String? { - guard sections[section].id == .aboutSection, hasFullAccess else { return nil } - return LocalizedString.getValue("settings.fullVersion.footer") - } - - private var hasFullAccess: Bool { - cryptomatorSettings.hasRunningSubscription || cryptomatorSettings.fullVersionUnlocked - } - private var aboutSectionElements: [TableViewCellViewModel] { var elements: [TableViewCellViewModel] = [ ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator")) @@ -95,28 +95,25 @@ class SettingsViewModel: TableViewModel { return elements } - private let cacheSizeCellViewModel = LoadingWithLabelCellViewModel(title: LocalizedString.getValue("settings.cacheSize")) - private let clearCacheButtonCellViewModel = ButtonCellViewModel(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false) - - private var cryptomatorSettings: CryptomatorSettings - - private var purchaseStatusCellViewModel: PurchaseStatusCellViewModel { - let subtitle: String + private var purchaseStatusCellViewModel: ButtonCellViewModel { + let detailTitle: String if let trialExpirationDate = cryptomatorSettings.trialExpirationDate, trialExpirationDate > Date() { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .none - subtitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate)) + detailTitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate)) } else { - subtitle = LocalizedString.getValue("settings.freeTier.subtitle") + detailTitle = LocalizedString.getValue("settings.freeTier.subtitle") } - return PurchaseStatusCellViewModel( - iconName: "checkmark.seal.fill", - title: LocalizedString.getValue("settings.unlockFullVersion"), - subtitle: subtitle - ) + let image = UIImage(systemName: "checkmark.seal.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22)) + return ButtonCellViewModel.createDisclosureButton(action: .showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion"), detailTitle: detailTitle, image: image, cellStyle: .subtitle) } + private let cacheSizeCellViewModel = LoadingWithLabelCellViewModel(title: LocalizedString.getValue("settings.cacheSize")) + private let clearCacheButtonCellViewModel = ButtonCellViewModel(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false) + + private var cryptomatorSettings: CryptomatorSettings + private lazy var debugModeViewModel: SwitchCellViewModel = { let viewModel = SwitchCellViewModel(title: LocalizedString.getValue("settings.debugMode"), isOn: cryptomatorSettings.debugModeEnabled) bindDebugModeViewModel(viewModel) diff --git a/Cryptomator/VaultDetail/VaultDetailViewController.swift b/Cryptomator/VaultDetail/VaultDetailViewController.swift index bda83927a..1912c5e6b 100644 --- a/Cryptomator/VaultDetail/VaultDetailViewController.swift +++ b/Cryptomator/VaultDetail/VaultDetailViewController.swift @@ -106,7 +106,7 @@ class VaultDetailViewController: BaseUITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cellViewModel = viewModel.cellViewModel(for: indexPath) - let cell = cellViewModel.type.init() + let cell = cellViewModel.type.init(style: cellViewModel.cellStyle, reuseIdentifier: nil) cell.configure(with: cellViewModel) return cell } From 6ecbdad28a34f4d0cb10f28aac903103fe2327c1 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 24 Nov 2025 17:45:05 +0100 Subject: [PATCH 9/9] Refactor settings to use unlockFullVersion terminology --- Cryptomator/Purchase/Cells/TrialCell.swift | 3 +-- Cryptomator/Purchase/PurchaseAlert.swift | 3 +-- Cryptomator/Settings/SettingsViewModel.swift | 24 +++++++++----------- SharedResources/en.lproj/Localizable.strings | 6 ++--- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Cryptomator/Purchase/Cells/TrialCell.swift b/Cryptomator/Purchase/Cells/TrialCell.swift index 413400497..cba4e6c1f 100644 --- a/Cryptomator/Purchase/Cells/TrialCell.swift +++ b/Cryptomator/Purchase/Cells/TrialCell.swift @@ -76,8 +76,7 @@ struct TrialCellViewModel: Hashable { var expirationText: String { let formatter = DateFormatter() formatter.dateStyle = .short - let formattedExpirationDate = formatter.string(for: expirationDate) ?? "Invalid Date" - return String(format: LocalizedString.getValue("purchase.product.trial.expirationDate"), formattedExpirationDate) + return String(format: LocalizedString.getValue("purchase.product.trial.expirationDate"), formatter.string(from: expirationDate)) } let expirationDate: Date diff --git a/Cryptomator/Purchase/PurchaseAlert.swift b/Cryptomator/Purchase/PurchaseAlert.swift index 66c7658b6..74318779b 100644 --- a/Cryptomator/Purchase/PurchaseAlert.swift +++ b/Cryptomator/Purchase/PurchaseAlert.swift @@ -18,8 +18,7 @@ enum PurchaseAlert { static func showForTrial(title: String, expirationDate: Date, on presentingViewController: UIViewController) -> Promise { let formatter = DateFormatter() formatter.dateStyle = .short - let formattedExpireDate = formatter.string(for: expirationDate) ?? "Invalid Date" - let message = String(format: LocalizedString.getValue("purchase.restorePurchase.validTrialFound.alert.message"), formattedExpireDate) + let message = String(format: LocalizedString.getValue("purchase.restorePurchase.validTrialFound.alert.message"), formatter.string(from: expirationDate)) return showAlert(title: title, message: message, on: presentingViewController) } diff --git a/Cryptomator/Settings/SettingsViewModel.swift b/Cryptomator/Settings/SettingsViewModel.swift index 7c524454e..4b72f9cc8 100644 --- a/Cryptomator/Settings/SettingsViewModel.swift +++ b/Cryptomator/Settings/SettingsViewModel.swift @@ -28,7 +28,7 @@ enum SettingsButtonAction: String { } enum SettingsSection: Int { - case purchaseStatusSection = 0 + case unlockFullVersionSection = 0 case cloudServiceSection case cacheSection case aboutSection @@ -46,22 +46,22 @@ class SettingsViewModel: TableViewModel { } override func getFooterTitle(for section: Int) -> String? { - guard sections[section].id == .aboutSection, hasFullAccess else { return nil } - return LocalizedString.getValue("settings.fullVersion.footer") + guard sections[section].id == .aboutSection, hasFullVersion else { return nil } + return LocalizedString.getValue("settings.aboutCryptomator.hasFullVersion.footer") } var showDebugModeWarning: AnyPublisher { return showDebugModeWarningPublisher.eraseToAnyPublisher() } - private var hasFullAccess: Bool { + private var hasFullVersion: Bool { cryptomatorSettings.hasRunningSubscription || cryptomatorSettings.fullVersionUnlocked } private var _sections: [Section] { var sections: [Section] = [] - if !hasFullAccess { - sections.append(Section(id: .purchaseStatusSection, elements: [purchaseStatusCellViewModel])) + if !hasFullVersion { + sections.append(Section(id: .unlockFullVersionSection, elements: [unlockFullVersionCellViewModel])) } sections.append(contentsOf: [ Section(id: .cloudServiceSection, elements: [ @@ -95,15 +95,14 @@ class SettingsViewModel: TableViewModel { return elements } - private var purchaseStatusCellViewModel: ButtonCellViewModel { + private var unlockFullVersionCellViewModel: ButtonCellViewModel { let detailTitle: String if let trialExpirationDate = cryptomatorSettings.trialExpirationDate, trialExpirationDate > Date() { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none - detailTitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate)) + let formatter = DateFormatter() + formatter.dateStyle = .short + detailTitle = String(format: LocalizedString.getValue("settings.unlockFullVersion.trialExpirationDate"), formatter.string(from: trialExpirationDate)) } else { - detailTitle = LocalizedString.getValue("settings.freeTier.subtitle") + detailTitle = LocalizedString.getValue("settings.unlockFullVersion.detail") } let image = UIImage(systemName: "checkmark.seal.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22)) return ButtonCellViewModel.createDisclosureButton(action: .showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion"), detailTitle: detailTitle, image: image, cellStyle: .subtitle) @@ -111,7 +110,6 @@ class SettingsViewModel: TableViewModel { private let cacheSizeCellViewModel = LoadingWithLabelCellViewModel(title: LocalizedString.getValue("settings.cacheSize")) private let clearCacheButtonCellViewModel = ButtonCellViewModel(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false) - private var cryptomatorSettings: CryptomatorSettings private lazy var debugModeViewModel: SwitchCellViewModel = { diff --git a/SharedResources/en.lproj/Localizable.strings b/SharedResources/en.lproj/Localizable.strings index 6faca5687..06a4b3884 100644 --- a/SharedResources/en.lproj/Localizable.strings +++ b/SharedResources/en.lproj/Localizable.strings @@ -202,6 +202,7 @@ "settings.title" = "Settings"; "settings.aboutCryptomator" = "About Cryptomator"; "settings.aboutCryptomator.title" = "Version %@ (%@)"; +"settings.aboutCryptomator.hasFullVersion.footer" = "You have unlocked the full version and gained write access to your vaults."; "settings.cacheSize" = "Cache Size"; "settings.clearCache" = "Clear Cache"; "settings.cloudServices" = "Cloud Services"; @@ -213,9 +214,8 @@ "settings.sendLogFile" = "Send Log File"; "settings.shortcutsGuide" = "Shortcuts Guide"; "settings.unlockFullVersion" = "Unlock Full Version"; -"settings.fullVersion.footer" = "You have unlocked the full version and gained write access to your vaults."; -"settings.trial.expirationDate" = "Trial Expiration Date: %@"; -"settings.freeTier.subtitle" = "Gain write access to your vaults."; +"settings.unlockFullVersion.detail" = "Gain write access to your vaults."; +"settings.unlockFullVersion.trialExpirationDate" = "Trial Expiration Date: %@"; "sharePoint.enterURL.title" = "Enter SharePoint URL"; "sharePoint.enterURL.placeholder" = "SharePoint Site URL";