Skip to content

Commit

Permalink
Merge branch 'release/2.6.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
tobihagemann committed Nov 30, 2024
2 parents d2d1825 + f565e7f commit 5b9104b
Show file tree
Hide file tree
Showing 20 changed files with 309 additions and 76 deletions.
4 changes: 2 additions & 2 deletions Cryptomator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3320,7 +3320,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.6.3;
MARKETING_VERSION = 2.6.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -3382,7 +3382,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.6.3;
MARKETING_VERSION = 2.6.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200 -Xfrontend -warn-long-function-bodies=200";
Expand Down
18 changes: 17 additions & 1 deletion Cryptomator/MainCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import CryptomatorCommonCore
import Promises
import StoreKit
import UIKit

class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
Expand Down Expand Up @@ -77,6 +78,15 @@ class MainCoordinator: NSObject, Coordinator, UINavigationControllerDelegate {
rootViewController.showDetailViewController(detailNavigationController, sender: nil)
}

// Temporarily added for December 2024 Sale
func showPurchase() {
let modalNavigationController = BaseNavigationController()
let child = PurchaseCoordinator(navigationController: modalNavigationController)
childCoordinators.append(child)
navigationController.topViewController?.present(modalNavigationController, animated: true)
child.start()
}

// MARK: - UINavigationControllerDelegate

func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
Expand Down Expand Up @@ -114,6 +124,8 @@ extension MainCoordinator: StoreObserverDelegate {
switch transaction {
case .fullVersion, .yearlySubscription:
showFullVersionAlert()
// Temporarily added for December 2024 Sale
NotificationCenter.default.post(name: .purchasedFullVersionNotification, object: nil)
case let .freeTrial(expiresOn):
showTrialAlert(expirationDate: expiresOn)
case .unknown:
Expand All @@ -126,7 +138,11 @@ extension MainCoordinator: StoreObserverDelegate {
guard let navigationController = self?.navigationController else {
return
}
_ = PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController)
PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController).then {
if let windowScene = navigationController.view.window?.windowScene {
SKStoreReviewController.requestReview(in: windowScene)
}
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions Cryptomator/Purchase/Cells/PurchaseCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UIKit

struct PurchaseCellViewModel: Hashable {
let productName: String
let productDetail: String?
let price: String
let purchaseDetail: String?
let purchaseButtonViewModel = PurchaseButtonViewModel()
Expand All @@ -36,6 +37,7 @@ class PurchaseCell: IAPCell {

func configure(with viewModel: PurchaseCellViewModel) {
productTitleLabel.text = viewModel.productName
productDetailLabel.text = viewModel.productDetail
accessory.button.setTitle(viewModel.price, for: .normal)
accessory.detailLabel.text = viewModel.purchaseDetail
accessory.configure(with: viewModel.purchaseButtonViewModel)
Expand Down
5 changes: 5 additions & 0 deletions Cryptomator/Purchase/PurchaseCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ class PurchaseCoordinator: Coordinator {

func fullVersionPurchased() {
PurchaseAlert.showForFullVersion(title: LocalizedString.getValue("purchase.unlockedFullVersion.title"), on: navigationController).then {
if let windowScene = self.navigationController.view.window?.windowScene {
SKStoreReviewController.requestReview(in: windowScene)
}
self.unlockedPro()
}
// Temporarily added for December 2024 Sale
NotificationCenter.default.post(name: .purchasedFullVersionNotification, object: nil)
}

func handleRestoreResult(_ result: RestoreTransactionsResult) {
Expand Down
20 changes: 20 additions & 0 deletions Cryptomator/Purchase/PurchaseViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,23 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
return LocalizedString.getValue("purchase.title")
}

// Temporarily added for December 2024 Sale
override var infoText: NSAttributedString? {
let currentYear = Calendar.current.component(.year, from: Date())
let currentMonth = Calendar.current.component(.month, from: Date())
if currentYear == 2024 && currentMonth == 12 {
return NSAttributedString(
string: "*Note: The discount amount may vary by region.",
attributes: [
.font: UIFont.preferredFont(forTextStyle: .footnote),
.foregroundColor: UIColor.secondaryLabel
]
)
} else {
return nil
}
}

private let cryptomatorSettings: CryptomatorSettings

init(storeManager: IAPStore = StoreManager.shared, iapManager: IAPManager = StoreObserver.shared, cryptomatorSettings: CryptomatorSettings = CryptomatorUserDefaults.shared, minimumDisplayTime: TimeInterval = 1.0) {
Expand All @@ -56,6 +73,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
cells.append(.trialCell(TrialCellViewModel(expirationDate: trialExpirationDate)))
} else {
cells.append(.purchaseCell(PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.trial"),
productDetail: nil,
price: LocalizedString.getValue("purchase.product.pricing.free"),
purchaseDetail: LocalizedString.getValue("purchase.product.trial.duration"),
productIdentifier: .thirtyDayTrial)))
Expand All @@ -65,6 +83,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
private func addSubscriptionItem() {
if let product = products[.yearlySubscription], let localizedPrice = product.localizedPrice {
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.yearlySubscription"),
productDetail: nil,
price: localizedPrice,
purchaseDetail: LocalizedString.getValue("purchase.product.yearlySubscription.duration"),
productIdentifier: .yearlySubscription)
Expand All @@ -75,6 +94,7 @@ class PurchaseViewModel: BaseIAPViewModel, ProductFetching {
private func addLifetimeLicenseItem() {
if let product = products[.fullVersion], let localizedPrice = product.localizedPrice {
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.lifetimeLicense"),
productDetail: "🎁 33%* off in December",
price: localizedPrice,
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
productIdentifier: .fullVersion)
Expand Down
2 changes: 2 additions & 0 deletions Cryptomator/Purchase/UpgradeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class UpgradeViewModel: BaseIAPViewModel, ProductFetching {
func addFreeUpgradeItem() {
guard products[.freeUpgrade] != nil else { return }
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.freeUpgrade"),
productDetail: nil,
price: LocalizedString.getValue("purchase.product.pricing.free"),
purchaseDetail: nil,
productIdentifier: .freeUpgrade)
Expand All @@ -43,6 +44,7 @@ class UpgradeViewModel: BaseIAPViewModel, ProductFetching {
func addPaidUpgradeItem() {
if let product = products[.paidUpgrade], let localizedPrice = product.localizedPrice {
let viewModel = PurchaseCellViewModel(productName: LocalizedString.getValue("purchase.product.donateAndUpgrade"),
productDetail: nil,
price: localizedPrice,
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
productIdentifier: .paidUpgrade)
Expand Down
107 changes: 105 additions & 2 deletions Cryptomator/VaultList/VaultListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ class VaultListViewController: ListViewController<VaultCellViewModel> {
weak var coordinator: MainCoordinator?

private let viewModel: VaultListViewModelProtocol
private var observer: NSObjectProtocol?
private var willEnterForegroundObserver: NSObjectProtocol?
@Dependency(\.fullVersionChecker) private var fullVersionChecker
@Dependency(\.cryptomatorSettings) private var cryptomatorSettings

#if !ALWAYS_PREMIUM
private var bannerView: UIView?
private var fullVersionPurchasedObserver: NSObjectProtocol?
#endif

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

observer = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
willEnterForegroundObserver = NotificationCenter.default.addObserver(forName: UIApplication.willEnterForegroundNotification, object: nil, queue: .main) { [weak self] _ in
self?.viewModel.refreshVaultLockStates().catch { error in
DDLogError("Refresh vault lock states failed with error: \(error)")
}
}

#if !ALWAYS_PREMIUM
fullVersionPurchasedObserver = NotificationCenter.default.addObserver(forName: .purchasedFullVersionNotification, object: nil, queue: .main) { [weak self] _ in
self?.dismissBanner()
}
checkAndShowBanner()
#endif
}

override func viewWillAppear(_ animated: Bool) {
Expand Down Expand Up @@ -108,4 +121,94 @@ class VaultListViewController: ListViewController<VaultCellViewModel> {
coordinator?.showVaultDetail(for: vaultCellViewModel.vault)
}
}

// MARK: - Discount Banner

#if !ALWAYS_PREMIUM
private func checkAndShowBanner() {
let currentYear = Calendar.current.component(.year, from: Date())
let currentMonth = Calendar.current.component(.month, from: Date())
if currentYear == 2024, currentMonth == 12, !(cryptomatorSettings.fullVersionUnlocked || cryptomatorSettings.hasRunningSubscription), !cryptomatorSettings.december2024BannerDismissed {
showBanner()
}
}

private func showBanner() {
let banner = UIView()
banner.backgroundColor = UIColor.cryptomatorPrimary
banner.translatesAutoresizingMaskIntoConstraints = false
banner.layer.cornerRadius = 12
banner.layer.masksToBounds = true

let emojiLabel = UILabel()
emojiLabel.text = "🎁"
emojiLabel.translatesAutoresizingMaskIntoConstraints = false
emojiLabel.setContentHuggingPriority(.required, for: .horizontal)
emojiLabel.setContentCompressionResistancePriority(.required, for: .horizontal)

let textLabel = UILabel()
textLabel.text = "Lifetime License is 33%* off in December!"
textLabel.textColor = .white
textLabel.font = UIFont.preferredFont(forTextStyle: .footnote)
textLabel.adjustsFontSizeToFitWidth = true
textLabel.minimumScaleFactor = 0.5
textLabel.numberOfLines = 2
textLabel.translatesAutoresizingMaskIntoConstraints = false

let dismissButton = UIButton(type: .close)
dismissButton.addTarget(self, action: #selector(dismissBanner), for: .touchUpInside)
dismissButton.translatesAutoresizingMaskIntoConstraints = false
dismissButton.setContentHuggingPriority(.required, for: .horizontal)
dismissButton.setContentCompressionResistancePriority(.required, for: .horizontal)

banner.addSubview(emojiLabel)
banner.addSubview(textLabel)
banner.addSubview(dismissButton)

NSLayoutConstraint.activate([
emojiLabel.leadingAnchor.constraint(equalTo: banner.leadingAnchor, constant: 16),
emojiLabel.centerYAnchor.constraint(equalTo: banner.centerYAnchor),

textLabel.leadingAnchor.constraint(equalTo: emojiLabel.trailingAnchor, constant: 8),
textLabel.centerYAnchor.constraint(equalTo: banner.centerYAnchor),

dismissButton.leadingAnchor.constraint(equalTo: textLabel.trailingAnchor, constant: 8),
dismissButton.trailingAnchor.constraint(equalTo: banner.trailingAnchor, constant: -16),
dismissButton.centerYAnchor.constraint(equalTo: banner.centerYAnchor)
])

let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(bannerTapped))
banner.addGestureRecognizer(tapGestureRecognizer)

view.addSubview(banner)

NSLayoutConstraint.activate([
banner.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
banner.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
banner.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
banner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
banner.heightAnchor.constraint(equalToConstant: 50)
])

bannerView = banner
}

@objc private func dismissBanner() {
UIView.animate(withDuration: 0.3, animations: {
self.bannerView?.alpha = 0
}, completion: { _ in
self.bannerView?.removeFromSuperview()
self.bannerView = nil
})
CryptomatorUserDefaults.shared.december2024BannerDismissed = true
}

@objc private func bannerTapped() {
coordinator?.showPurchase()
}
#endif
}

extension Notification.Name {
static let purchasedFullVersionNotification = Notification.Name("PurchasedFullVersionNotification")
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public protocol CryptomatorSettings {
var trialExpirationDate: Date? { get set }
var fullVersionUnlocked: Bool { get set }
var hasRunningSubscription: Bool { get set }
#if !ALWAYS_PREMIUM
var december2024BannerDismissed: Bool { get set }
#endif
}

private enum CryptomatorSettingsKey: DependencyKey {
Expand Down Expand Up @@ -108,4 +111,11 @@ extension CryptomatorUserDefaults: CryptomatorSettings {
get { read() ?? false }
set { write(value: newValue) }
}

#if !ALWAYS_PREMIUM
public var december2024BannerDismissed: Bool {
get { read() ?? false }
set { write(value: newValue) }
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ class CryptomatorSettingsMock: CryptomatorSettings {
var debugModeEnabled: Bool = false
var fullVersionUnlocked: Bool = false
var hasRunningSubscription: Bool = false
#if !ALWAYS_PREMIUM
var december2024BannerDismissed: Bool = false
#endif
}
#endif
3 changes: 3 additions & 0 deletions CryptomatorTests/Purchase/PurchaseViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,20 +153,23 @@ class PurchaseViewModelTests: IAPViewModelTestCase {

private var purchaseTrialCell: Item {
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.trial"),
productDetail: nil,
price: LocalizedString.getValue("purchase.product.pricing.free"),
purchaseDetail: LocalizedString.getValue("purchase.product.trial.duration"),
productIdentifier: .thirtyDayTrial))
}

private var yearlySubscriptionCell: Item {
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.yearlySubscription"),
productDetail: nil,
price: "$5.99",
purchaseDetail: LocalizedString.getValue("purchase.product.yearlySubscription.duration"),
productIdentifier: .yearlySubscription))
}

private var lifetimeLicenseCell: Item {
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.lifetimeLicense"),
productDetail: "🎁 33%* off in December",
price: "$11.99",
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
productIdentifier: .fullVersion))
Expand Down
2 changes: 2 additions & 0 deletions CryptomatorTests/Purchase/UpgradeViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,15 @@ class UpgradeViewModelTests: IAPViewModelTestCase {

private var freeUpgradeCell: Item {
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.freeUpgrade"),
productDetail: nil,
price: LocalizedString.getValue("purchase.product.pricing.free"),
purchaseDetail: nil,
productIdentifier: .freeUpgrade))
}

private var paidUpgradeCell: Item {
return .purchaseCell(.init(productName: LocalizedString.getValue("purchase.product.donateAndUpgrade"),
productDetail: nil,
price: "$1.99",
purchaseDetail: LocalizedString.getValue("purchase.product.lifetimeLicense.duration"),
productIdentifier: .paidUpgrade))
Expand Down
Loading

0 comments on commit 5b9104b

Please sign in to comment.