Skip to content

Commit e94263d

Browse files
committed
Improve purchase status display in settings
1 parent b295ee2 commit e94263d

File tree

6 files changed

+156
-16
lines changed

6 files changed

+156
-16
lines changed

Cryptomator.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,8 @@
439439
74F5DC1C26DCD2FB00AFE989 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */; };
440440
74F5DC1F26DD036D00AFE989 /* StoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F5DC1E26DD036D00AFE989 /* StoreManager.swift */; };
441441
74FC576125ADED030003ED27 /* VaultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FC576025ADED030003ED27 /* VaultCell.swift */; };
442+
B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */; };
443+
B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */; };
442444
B330CB452CB5735300C21E03 /* UnauthorizedErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */; };
443445
B34C53262D142B1000F30FE9 /* EnterSharePointURLViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */; };
444446
B34C53282D142B5800F30FE9 /* EnterSharePointURLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */; };
@@ -1058,6 +1060,8 @@
10581060
74F5DC1B26DCD2FB00AFE989 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
10591061
74F5DC1E26DD036D00AFE989 /* StoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreManager.swift; sourceTree = "<group>"; };
10601062
74FC576025ADED030003ED27 /* VaultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultCell.swift; sourceTree = "<group>"; };
1063+
B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCell.swift; sourceTree = "<group>"; };
1064+
B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseStatusCellViewModel.swift; sourceTree = "<group>"; };
10611065
B330CB442CB5735000C21E03 /* UnauthorizedErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedErrorViewController.swift; sourceTree = "<group>"; };
10621066
B34C53252D142B0700F30FE9 /* EnterSharePointURLViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewController.swift; sourceTree = "<group>"; };
10631067
B34C53272D142B5400F30FE9 /* EnterSharePointURLViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterSharePointURLViewModel.swift; sourceTree = "<group>"; };
@@ -2037,6 +2041,8 @@
20372041
740D3683266A1B180058744D /* SettingsCoordinator.swift */,
20382042
740D367D266A18DF0058744D /* SettingsViewController.swift */,
20392043
740D3681266A19150058744D /* SettingsViewModel.swift */,
2044+
B32024D12ED0778800E82B07 /* PurchaseStatusCell.swift */,
2045+
B32024D22ED0778800E82B07 /* PurchaseStatusCellViewModel.swift */,
20402046
7408E6CB26779BC200D7FAEA /* About */,
20412047
);
20422048
path = Settings;
@@ -2783,6 +2789,8 @@
27832789
4A644B57267C958F008CBB9A /* ChildCoordinator.swift in Sources */,
27842790
4A53CC15267CC33100853BB3 /* CreateNewVaultPasswordViewModel.swift in Sources */,
27852791
4AA22C1E261CA94700A17486 /* UsernameFieldCell.swift in Sources */,
2792+
B32024D32ED0778800E82B07 /* PurchaseStatusCellViewModel.swift in Sources */,
2793+
B32024D42ED0778800E82B07 /* PurchaseStatusCell.swift in Sources */,
27862794
4A136132276770BB0077EB7F /* SnapshotVaultListViewModel.swift in Sources */,
27872795
4AED9A79286B4DF500352951 /* S3Authenticating.swift in Sources */,
27882796
747C35172762A3F500E4CA28 /* AttributedTextHeaderFooterViewModel.swift in Sources */,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// PurchaseStatusCell.swift
3+
// Cryptomator
4+
//
5+
// Created by Majid Achhoud on 20.11.24.
6+
// Copyright © 2024 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import Combine
10+
import CryptomatorCommonCore
11+
import UIKit
12+
13+
class PurchaseStatusCell: UITableViewCell, ConfigurableTableViewCell {
14+
private let iconImageView = UIImageView()
15+
private let titleLabel = UILabel()
16+
private let subtitleLabel = UILabel()
17+
lazy var subscribers = Set<AnyCancellable>()
18+
19+
func configure(with viewModel: TableViewCellViewModel) {
20+
guard let viewModel = viewModel as? PurchaseStatusCellViewModel else {
21+
return
22+
}
23+
iconImageView.image = UIImage(systemName: viewModel.iconName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 22))
24+
viewModel.title.$value.assign(to: \.text, on: titleLabel).store(in: &subscribers)
25+
viewModel.subtitle.$value.assign(to: \.text, on: subtitleLabel).store(in: &subscribers)
26+
accessoryType = .disclosureIndicator
27+
selectionStyle = .default
28+
}
29+
30+
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
31+
super.init(style: style, reuseIdentifier: reuseIdentifier)
32+
33+
iconImageView.translatesAutoresizingMaskIntoConstraints = false
34+
iconImageView.contentMode = .scaleAspectFit
35+
iconImageView.tintColor = .cryptomatorPrimary
36+
37+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
38+
titleLabel.font = .preferredFont(forTextStyle: .body)
39+
titleLabel.textColor = .label
40+
titleLabel.numberOfLines = 0
41+
42+
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
43+
subtitleLabel.font = .preferredFont(forTextStyle: .footnote)
44+
subtitleLabel.textColor = .secondaryLabel
45+
subtitleLabel.numberOfLines = 0
46+
47+
contentView.addSubview(iconImageView)
48+
contentView.addSubview(titleLabel)
49+
contentView.addSubview(subtitleLabel)
50+
51+
NSLayoutConstraint.activate([
52+
iconImageView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
53+
iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
54+
iconImageView.widthAnchor.constraint(equalToConstant: 29),
55+
iconImageView.heightAnchor.constraint(equalToConstant: 29),
56+
57+
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
58+
titleLabel.leadingAnchor.constraint(equalTo: iconImageView.trailingAnchor, constant: 12),
59+
titleLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
60+
61+
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4),
62+
subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
63+
subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
64+
subtitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -11)
65+
])
66+
}
67+
68+
@available(*, unavailable)
69+
required init?(coder: NSCoder) {
70+
fatalError("init(coder:) has not been implemented")
71+
}
72+
73+
func removeAllBindings() {
74+
subscribers.forEach { $0.cancel() }
75+
}
76+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// PurchaseStatusCellViewModel.swift
3+
// Cryptomator
4+
//
5+
// Created by Majid Achhoud on 20.11.24.
6+
// Copyright © 2024 Skymatic GmbH. All rights reserved.
7+
//
8+
9+
import CryptomatorCommonCore
10+
import UIKit
11+
12+
class PurchaseStatusCellViewModel: TableViewCellViewModel {
13+
override var type: ConfigurableTableViewCell.Type { PurchaseStatusCell.self }
14+
15+
let iconName: String
16+
let title: Bindable<String?>
17+
let subtitle: Bindable<String?>
18+
19+
init(iconName: String, title: String, subtitle: String) {
20+
self.iconName = iconName
21+
self.title = Bindable(title)
22+
self.subtitle = Bindable(subtitle)
23+
}
24+
}

Cryptomator/Settings/SettingsViewController.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ class SettingsViewController: StaticUITableViewController<SettingsSection> {
134134
case .none:
135135
break
136136
}
137+
138+
if dataSource?.itemIdentifier(for: indexPath) is PurchaseStatusCellViewModel {
139+
tableView.deselectRow(at: indexPath, animated: true)
140+
coordinator?.showUnlockFullVersion()
141+
}
137142
}
138143

139144
private func refreshRows() {

Cryptomator/Settings/SettingsViewModel.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ enum SettingsButtonAction: String {
2828
}
2929

3030
enum SettingsSection: Int {
31-
case cloudServiceSection = 0
31+
case purchaseStatusSection = 0
32+
case cloudServiceSection
3233
case cacheSection
3334
case aboutSection
3435
case debugSection
@@ -49,7 +50,11 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
4950
}
5051

5152
private var _sections: [Section<SettingsSection>] {
52-
return [
53+
var sections: [Section<SettingsSection>] = []
54+
if !hasFullAccess {
55+
sections.append(Section(id: .purchaseStatusSection, elements: [purchaseStatusCellViewModel]))
56+
}
57+
sections.append(contentsOf: [
5358
Section(id: .cloudServiceSection, elements: [
5459
ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showCloudServices, title: LocalizedString.getValue("settings.cloudServices"))
5560
]),
@@ -67,23 +72,25 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
6772
ButtonCellViewModel(action: SettingsButtonAction.showContact, title: LocalizedString.getValue("settings.contact")),
6873
ButtonCellViewModel(action: SettingsButtonAction.showRateApp, title: LocalizedString.getValue("settings.rateApp"))
6974
])
70-
]
75+
])
76+
return sections
7177
}
7278

73-
private var aboutSectionElements: [TableViewCellViewModel] {
74-
var elements: [TableViewCellViewModel] = [ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator"))]
79+
override func getFooterTitle(for section: Int) -> String? {
80+
guard sections[section].id == .aboutSection, hasFullAccess else { return nil }
81+
return LocalizedString.getValue("settings.fullVersion.footer")
82+
}
7583

84+
private var hasFullAccess: Bool {
85+
cryptomatorSettings.hasRunningSubscription || cryptomatorSettings.fullVersionUnlocked
86+
}
87+
88+
private var aboutSectionElements: [TableViewCellViewModel] {
89+
var elements: [TableViewCellViewModel] = [
90+
ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showAbout, title: LocalizedString.getValue("settings.aboutCryptomator"))
91+
]
7692
if cryptomatorSettings.hasRunningSubscription {
77-
elements.append(ButtonCellViewModel<SettingsButtonAction>(action: .showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions")))
78-
} else if cryptomatorSettings.fullVersionUnlocked {
79-
let statusCell = BindableTableViewCellViewModel(
80-
title: LocalizedString.getValue("settings.fullVersionStatus"),
81-
selectionStyle: .none,
82-
accessoryType: .checkmark
83-
)
84-
elements.append(statusCell)
85-
} else {
86-
elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showUnlockFullVersion, title: LocalizedString.getValue("settings.unlockFullVersion")))
93+
elements.append(ButtonCellViewModel.createDisclosureButton(action: SettingsButtonAction.showManageSubscriptions, title: LocalizedString.getValue("settings.manageSubscriptions")))
8794
}
8895
return elements
8996
}
@@ -92,6 +99,24 @@ class SettingsViewModel: TableViewModel<SettingsSection> {
9299
private let clearCacheButtonCellViewModel = ButtonCellViewModel<SettingsButtonAction>(action: .clearCache, title: LocalizedString.getValue("settings.clearCache"), isEnabled: false)
93100

94101
private var cryptomatorSettings: CryptomatorSettings
102+
103+
private var purchaseStatusCellViewModel: PurchaseStatusCellViewModel {
104+
let subtitle: String
105+
if let trialExpirationDate = cryptomatorSettings.trialExpirationDate, trialExpirationDate > Date() {
106+
let dateFormatter = DateFormatter()
107+
dateFormatter.dateStyle = .medium
108+
dateFormatter.timeStyle = .none
109+
subtitle = String(format: LocalizedString.getValue("settings.trial.expirationDate"), dateFormatter.string(from: trialExpirationDate))
110+
} else {
111+
subtitle = LocalizedString.getValue("settings.freeTier.subtitle")
112+
}
113+
return PurchaseStatusCellViewModel(
114+
iconName: "checkmark.seal.fill",
115+
title: LocalizedString.getValue("settings.unlockFullVersion"),
116+
subtitle: subtitle
117+
)
118+
}
119+
95120
private lazy var debugModeViewModel: SwitchCellViewModel = {
96121
let viewModel = SwitchCellViewModel(title: LocalizedString.getValue("settings.debugMode"), isOn: cryptomatorSettings.debugModeEnabled)
97122
bindDebugModeViewModel(viewModel)

SharedResources/en.lproj/Localizable.strings

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@
213213
"settings.sendLogFile" = "Send Log File";
214214
"settings.shortcutsGuide" = "Shortcuts Guide";
215215
"settings.unlockFullVersion" = "Unlock Full Version";
216-
"settings.fullVersionStatus" = "Full Version";
216+
"settings.fullVersion.footer" = "You have unlocked the full version and gained write access to your vaults.";
217+
"settings.trial.expirationDate" = "Trial Expiration Date: %@";
218+
"settings.freeTier.subtitle" = "Gain write access to your vaults.";
217219

218220
"sharePoint.enterURL.title" = "Enter SharePoint URL";
219221
"sharePoint.enterURL.placeholder" = "SharePoint Site URL";

0 commit comments

Comments
 (0)