Skip to content
8 changes: 8 additions & 0 deletions Cryptomator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1058,6 +1060,8 @@
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>"; };
B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCell.swift; sourceTree = "<group>"; };
B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCellViewModel.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>"; };
Expand Down Expand Up @@ -2037,6 +2041,8 @@
740D3683266A1B180058744D /* SettingsCoordinator.swift */,
740D367D266A18DF0058744D /* SettingsViewController.swift */,
740D3681266A19150058744D /* SettingsViewModel.swift */,
B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */,
B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */,
7408E6CB26779BC200D7FAEA /* About */,
);
path = Settings;
Expand Down Expand Up @@ -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 */,
Expand Down
76 changes: 76 additions & 0 deletions Cryptomator/Settings/PurchaseStatusCell.swift
Original file line number Diff line number Diff line change
@@ -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<AnyCancellable>()

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() }
}
}
24 changes: 24 additions & 0 deletions Cryptomator/Settings/PurchaseStatusCellViewModel.swift
Original file line number Diff line number Diff line change
@@ -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<String?>
let subtitle: Bindable<String?>

init(iconName: String, title: String, subtitle: String) {
self.iconName = iconName
self.title = Bindable(title)
self.subtitle = Bindable(subtitle)
}
}
5 changes: 5 additions & 0 deletions Cryptomator/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ class SettingsViewController: StaticUITableViewController<SettingsSection> {
case .none:
break
}

if dataSource?.itemIdentifier(for: indexPath) is PurchaseStatusCellViewModel {
tableView.deselectRow(at: indexPath, animated: true)
coordinator?.showUnlockFullVersion()
}
}

private func refreshRows() {
Expand Down
48 changes: 40 additions & 8 deletions Cryptomator/Settings/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ enum SettingsButtonAction: String {
}

enum SettingsSection: Int {
case cloudServiceSection = 0
case purchaseStatusSection = 0
case cloudServiceSection
case cacheSection
case aboutSection
case debugSection
Expand All @@ -49,7 +50,11 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
}

private var _sections: [Section<SettingsSection>] {
return [
var sections: [Section<SettingsSection>] = []
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"))
]),
Expand All @@ -67,16 +72,25 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
ButtonCellViewModel(action: SettingsButtonAction.showContact, title: LocalizedString.getValue("settings.contact")),
ButtonCellViewModel(action: SettingsButtonAction.showRateApp, title: LocalizedString.getValue("settings.rateApp"))
])
]
])
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 = [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.createDisclosureButton(action: SettingsButtonAction.showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion")))
elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions")))
}
return elements
}
Expand All @@ -85,6 +99,24 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
private let clearCacheButtonCellViewModel = ButtonCellViewModel<SettingsButtonAction>(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)
Expand Down
3 changes: 3 additions & 0 deletions SharedResources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@
"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.";

"sharePoint.enterURL.title" = "Enter SharePoint URL";
"sharePoint.enterURL.placeholder" = "SharePoint Site URL";
Expand Down
Loading