Skip to content

Commit 5b9104b

Browse files
committed
Merge branch 'release/2.6.4'
2 parents d2d1825 + f565e7f commit 5b9104b

20 files changed

+309
-76
lines changed

Cryptomator.xcodeproj/project.pbxproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -3320,7 +3320,7 @@
33203320
GCC_WARN_UNUSED_FUNCTION = YES;
33213321
GCC_WARN_UNUSED_VARIABLE = YES;
33223322
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
3323-
MARKETING_VERSION = 2.6.3;
3323+
MARKETING_VERSION = 2.6.4;
33243324
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
33253325
MTL_FAST_MATH = YES;
33263326
ONLY_ACTIVE_ARCH = YES;
@@ -3382,7 +3382,7 @@
33823382
GCC_WARN_UNUSED_FUNCTION = YES;
33833383
GCC_WARN_UNUSED_VARIABLE = YES;
33843384
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
3385-
MARKETING_VERSION = 2.6.3;
3385+
MARKETING_VERSION = 2.6.4;
33863386
MTL_ENABLE_DEBUG_INFO = NO;
33873387
MTL_FAST_MATH = YES;
33883388
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200";

Cryptomator/MainCoordinator.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import CryptomatorCommonCore
1010
import Promises
11+
import StoreKit
1112
import UIKit
1213

1314
class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
@@ -77,6 +78,15 @@ class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
7778
rootViewController.showDetailViewController(detailNavigationController, sender: nil)
7879
}
7980

81+
// Temporarily added for December 2024 Sale
82+
func showPurchase() {
83+
let modalNavigationController = BaseNavigationController()
84+
let child = PurchaseCoordinator(navigationController: modalNavigationController)
85+
childCoordinators.append(child)
86+
navigationController.topViewController?.present(modalNavigationController, animated: true)
87+
child.start()
88+
}
89+
8090
// MARK: - UINavigationControllerDelegate
8191

8292
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
@@ -114,6 +124,8 @@ extension MainCoordinator: StoreObserverDelegate {
114124
switch transaction {
115125
case .fullVersion, .yearlySubscription:
116126
showFullVersionAlert()
127+
// Temporarily added for December 2024 Sale
128+
NotificationCenter.default.post(name: .purchasedFullVersionNotification, object: nil)
117129
case let .freeTrial(expiresOn):
118130
showTrialAlert(expirationDate: expiresOn)
119131
case .unknown:
@@ -126,7 +138,11 @@ extension MainCoordinator: StoreObserverDelegate {
126138
guard let navigationController = self?.navigationController else {
127139
return
128140
}
129-
_ = PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController)
141+
PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController).then {
142+
if let windowScene = navigationController.view.window?.windowScene {
143+
SKStoreReviewController.requestReview(in: windowScene)
144+
}
145+
}
130146
}
131147
}
132148

Cryptomator/Purchase/Cells/PurchaseCell.swift

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import UIKit
1212

1313
struct PurchaseCellViewModel: Hashable {
1414
let productName: String
15+
let productDetail: String?
1516
let price: String
1617
let purchaseDetail: String?
1718
let purchaseButtonViewModel = PurchaseButtonViewModel()
@@ -36,6 +37,7 @@ class PurchaseCell: IAPCell {
3637

3738
func configure(with viewModel: PurchaseCellViewModel) {
3839
productTitleLabel.text = viewModel.productName
40+
productDetailLabel.text = viewModel.productDetail
3941
accessory.button.setTitle(viewModel.price, for: .normal)
4042
accessory.detailLabel.text = viewModel.purchaseDetail
4143
accessory.configure(with: viewModel.purchaseButtonViewModel)

Cryptomator/Purchase/PurchaseCoordinator.swift

+5
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,13 @@ class PurchaseCoordinator: Coordinator {
4949

5050
func fullVersionPurchased() {
5151
PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController).then {
52+
if let windowScene = self.navigationController.view.window?.windowScene {
53+
SKStoreReviewController.requestReview(in: windowScene)
54+
}
5255
self.unlockedPro()
5356
}
57+
// Temporarily added for December 2024 Sale
58+
NotificationCenter.default.post(name: .purchasedFullVersionNotification, object: nil)
5459
}
5560

5661
func handleRestoreResult(_ result: RestoreTransactionsResult) {

Cryptomator/Purchase/PurchaseViewModel.swift

+20
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
3333
return LocalizedString.getValue("purchase.title")
3434
}
3535

36+
// Temporarily added for December 2024 Sale
37+
override var infoText: NSAttributedString? {
38+
let currentYear = Calendar.current.component(.year, from: Date())
39+
let currentMonth = Calendar.current.component(.month, from: Date())
40+
if currentYear == 2024 && currentMonth == 12 {
41+
return NSAttributedString(
42+
string: "*Note: The discount amount may vary by region.",
43+
attributes: [
44+
.font: UIFont.preferredFont(forTextStyle: .footnote),
45+
.foregroundColor: UIColor.secondaryLabel
46+
]
47+
)
48+
} else {
49+
return nil
50+
}
51+
}
52+
3653
private let cryptomatorSettings: CryptomatorSettings
3754

3855
init(storeManager: IAPStore = StoreManager.shared, iapManager: IAPManager = StoreObserver.shared, cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared, minimumDisplayTime: TimeInterval = 1.0) {
@@ -56,6 +73,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
5673
cells.append(.trialCell(TrialCellViewModel(expirationDate: trialExpirationDate)))
5774
} else {
5875
cells.append(.purchaseCell(PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.trial"),
76+
productDetail: nil,
5977
price: LocalizedString.getValue("purchase.product.pricing.free"),
6078
purchaseDetail: LocalizedString.getValue("purchase.product.trial.duration"),
6179
productIdentifier: .thirtyDayTrial)))
@@ -65,6 +83,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
6583
private func addSubscriptionItem() {
6684
if let product = products[.yearlySubscription], let localizedPrice = product.localizedPrice {
6785
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.yearlySubscription"),
86+
productDetail: nil,
6887
price: localizedPrice,
6988
purchaseDetail: LocalizedString.getValue("purchase.product.yearlySubscription.duration"),
7089
productIdentifier: .yearlySubscription)
@@ -75,6 +94,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
7594
private func addLifetimeLicenseItem() {
7695
if let product = products[.fullVersion], let localizedPrice = product.localizedPrice {
7796
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.lifetimeLicense"),
97+
productDetail: "🎁 33%* off in December",
7898
price: localizedPrice,
7999
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
80100
productIdentifier: .fullVersion)

Cryptomator/Purchase/UpgradeViewModel.swift

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class UpgradeViewModel: BaseIAPViewModel, ProductFetching {
3434
func addFreeUpgradeItem() {
3535
guard products[.freeUpgrade] != nil else { return }
3636
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.freeUpgrade"),
37+
productDetail: nil,
3738
price: LocalizedString.getValue("purchase.product.pricing.free"),
3839
purchaseDetail: nil,
3940
productIdentifier: .freeUpgrade)
@@ -43,6 +44,7 @@ class UpgradeViewModel: BaseIAPViewModel, ProductFetching {
4344
func addPaidUpgradeItem() {
4445
if let product = products[.paidUpgrade], let localizedPrice = product.localizedPrice {
4546
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.donateAndUpgrade"),
47+
productDetail: nil,
4648
price: localizedPrice,
4749
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
4850
productIdentifier: .paidUpgrade)

Cryptomator/VaultList/VaultListViewController.swift

+105-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ class VaultListViewController: ListViewController<VaultCellViewModel> {
1717
weak var coordinator: MainCoordinator?
1818

1919
private let viewModel: VaultListViewModelProtocol
20-
private var observer: NSObjectProtocol?
20+
private var willEnterForegroundObserver: NSObjectProtocol?
2121
@Dependency(\.fullVersionChecker) private var fullVersionChecker
22+
@Dependency(\.cryptomatorSettings) private var cryptomatorSettings
23+
24+
#if !ALWAYS_PREMIUM
25+
private var bannerView: UIView?
26+
private var fullVersionPurchasedObserver: NSObjectProtocol?
27+
#endif
2228

2329
init(with viewModel: VaultListViewModelProtocol) {
2430
self.viewModel = viewModel
@@ -44,11 +50,18 @@ class VaultListViewController: ListViewController<VaultCellViewModel> {
4450
let addNewVaulButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewVault))
4551
navigationItem.rightBarButtonItem = addNewVaulButton
4652

47-
observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
53+
willEnterForegroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
4854
self?.viewModel.refreshVaultLockStates().catch { error in
4955
DDLogError("Refresh vault lock states failed with error: \(error)")
5056
}
5157
}
58+
59+
#if !ALWAYS_PREMIUM
60+
fullVersionPurchasedObserver = NotificationCenter.default.addObserver(forName: .purchasedFullVersionNotification, object: nil, queue: .main) { [weak self] _ in
61+
self?.dismissBanner()
62+
}
63+
checkAndShowBanner()
64+
#endif
5265
}
5366

5467
override func viewWillAppear(_ animated: Bool) {
@@ -108,4 +121,94 @@ class VaultListViewController: ListViewController<VaultCellViewModel> {
108121
coordinator?.showVaultDetail(for: vaultCellViewModel.vault)
109122
}
110123
}
124+
125+
// MARK: - Discount Banner
126+
127+
#if !ALWAYS_PREMIUM
128+
private func checkAndShowBanner() {
129+
let currentYear = Calendar.current.component(.year, from: Date())
130+
let currentMonth = Calendar.current.component(.month, from: Date())
131+
if currentYear == 2024, currentMonth == 12, !(cryptomatorSettings.fullVersionUnlocked || cryptomatorSettings.hasRunningSubscription), !cryptomatorSettings.december2024BannerDismissed {
132+
showBanner()
133+
}
134+
}
135+
136+
private func showBanner() {
137+
let banner = UIView()
138+
banner.backgroundColor = UIColor.cryptomatorPrimary
139+
banner.translatesAutoresizingMaskIntoConstraints = false
140+
banner.layer.cornerRadius = 12
141+
banner.layer.masksToBounds = true
142+
143+
let emojiLabel = UILabel()
144+
emojiLabel.text = "🎁"
145+
emojiLabel.translatesAutoresizingMaskIntoConstraints = false
146+
emojiLabel.setContentHuggingPriority(.required, for: .horizontal)
147+
emojiLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
148+
149+
let textLabel = UILabel()
150+
textLabel.text = "Lifetime License is 33%* off in December!"
151+
textLabel.textColor = .white
152+
textLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
153+
textLabel.adjustsFontSizeToFitWidth = true
154+
textLabel.minimumScaleFactor = 0.5
155+
textLabel.numberOfLines = 2
156+
textLabel.translatesAutoresizingMaskIntoConstraints = false
157+
158+
let dismissButton = UIButton(type: .close)
159+
dismissButton.addTarget(self, action: #selector(dismissBanner), for: .touchUpInside)
160+
dismissButton.translatesAutoresizingMaskIntoConstraints = false
161+
dismissButton.setContentHuggingPriority(.required, for: .horizontal)
162+
dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)
163+
164+
banner.addSubview(emojiLabel)
165+
banner.addSubview(textLabel)
166+
banner.addSubview(dismissButton)
167+
168+
NSLayoutConstraint.activate([
169+
emojiLabel.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 16),
170+
emojiLabel.centerYAnchor.constraint(equalTo: banner.centerYAnchor),
171+
172+
textLabel.leadingAnchor.constraint(equalTo: emojiLabel.trailingAnchor, constant: 8),
173+
textLabel.centerYAnchor.constraint(equalTo: banner.centerYAnchor),
174+
175+
dismissButton.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 8),
176+
dismissButton.trailingAnchor.constraint(equalTo: banner.trailingAnchor, constant: -16),
177+
dismissButton.centerYAnchor.constraint(equalTo: banner.centerYAnchor)
178+
])
179+
180+
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(bannerTapped))
181+
banner.addGestureRecognizer(tapGestureRecognizer)
182+
183+
view.addSubview(banner)
184+
185+
NSLayoutConstraint.activate([
186+
banner.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
187+
banner.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
188+
banner.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
189+
banner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
190+
banner.heightAnchor.constraint(equalToConstant: 50)
191+
])
192+
193+
bannerView = banner
194+
}
195+
196+
@objc private func dismissBanner() {
197+
UIView.animate(withDuration: 0.3, animations: {
198+
self.bannerView?.alpha = 0
199+
}, completion: { _ in
200+
self.bannerView?.removeFromSuperview()
201+
self.bannerView = nil
202+
})
203+
CryptomatorUserDefaults.shared.december2024BannerDismissed = true
204+
}
205+
206+
@objc private func bannerTapped() {
207+
coordinator?.showPurchase()
208+
}
209+
#endif
210+
}
211+
212+
extension Notification.Name {
213+
static let purchasedFullVersionNotification = Notification.Name("PurchasedFullVersionNotification")
111214
}

CryptomatorCommon/Sources/CryptomatorCommonCore/CryptomatorUserDefaults.swift

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public protocol CryptomatorSettings {
1515
var trialExpirationDate: Date? { get set }
1616
var fullVersionUnlocked: Bool { get set }
1717
var hasRunningSubscription: Bool { get set }
18+
#if !ALWAYS_PREMIUM
19+
var december2024BannerDismissed: Bool { get set }
20+
#endif
1821
}
1922

2023
private enum CryptomatorSettingsKey: DependencyKey {
@@ -108,4 +111,11 @@ extension CryptomatorUserDefaults: CryptomatorSettings {
108111
get { read() ?? false }
109112
set { write(value: newValue) }
110113
}
114+
115+
#if !ALWAYS_PREMIUM
116+
public var december2024BannerDismissed: Bool {
117+
get { read() ?? false }
118+
set { write(value: newValue) }
119+
}
120+
#endif
111121
}

CryptomatorCommon/Sources/CryptomatorCommonCore/Mocks/CryptomatorSettingsMock.swift

+3
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ class CryptomatorSettingsMock: CryptomatorSettings {
1414
var debugModeEnabled: Bool = false
1515
var fullVersionUnlocked: Bool = false
1616
var hasRunningSubscription: Bool = false
17+
#if !ALWAYS_PREMIUM
18+
var december2024BannerDismissed: Bool = false
19+
#endif
1720
}
1821
#endif

CryptomatorTests/Purchase/PurchaseViewModelTests.swift

+3
Original file line numberDiff line numberDiff line change
@@ -153,20 +153,23 @@ class PurchaseViewModelTests: IAPViewModelTestCase {
153153

154154
private var purchaseTrialCell: Item {
155155
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.trial"),
156+
productDetail: nil,
156157
price: LocalizedString.getValue("purchase.product.pricing.free"),
157158
purchaseDetail: LocalizedString.getValue("purchase.product.trial.duration"),
158159
productIdentifier: .thirtyDayTrial))
159160
}
160161

161162
private var yearlySubscriptionCell: Item {
162163
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.yearlySubscription"),
164+
productDetail: nil,
163165
price: "$5.99",
164166
purchaseDetail: LocalizedString.getValue("purchase.product.yearlySubscription.duration"),
165167
productIdentifier: .yearlySubscription))
166168
}
167169

168170
private var lifetimeLicenseCell: Item {
169171
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.lifetimeLicense"),
172+
productDetail: "🎁 33%* off in December",
170173
price: "$11.99",
171174
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
172175
productIdentifier: .fullVersion))

CryptomatorTests/Purchase/UpgradeViewModelTests.swift

+2
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,15 @@ class UpgradeViewModelTests: IAPViewModelTestCase {
7777

7878
private var freeUpgradeCell: Item {
7979
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.freeUpgrade"),
80+
productDetail: nil,
8081
price: LocalizedString.getValue("purchase.product.pricing.free"),
8182
purchaseDetail: nil,
8283
productIdentifier: .freeUpgrade))
8384
}
8485

8586
private var paidUpgradeCell: Item {
8687
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.donateAndUpgrade"),
88+
productDetail: nil,
8789
price: "$1.99",
8890
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
8991
productIdentifier: .paidUpgrade))

0 commit comments

Comments
 (0)