|
|
@@ -30,6 +30,7 @@ private enum SettingsAction: Int {
|
|
|
case support = 2
|
|
|
case moreApps = 3
|
|
|
case shareApp = 4
|
|
|
+ case upgrade = 5
|
|
|
}
|
|
|
|
|
|
private enum PremiumPlan: Int {
|
|
|
@@ -84,6 +85,13 @@ private final class StoreKitCoordinator {
|
|
|
var onEntitlementsChanged: ((Bool) -> Void)?
|
|
|
|
|
|
var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
|
|
|
+ var hasLifetimeAccess: Bool { activeEntitlementProductIDs.contains(PremiumStoreProduct.lifetime) }
|
|
|
+ var activeNonLifetimePlan: PremiumPlan? {
|
|
|
+ activeEntitlementProductIDs
|
|
|
+ .compactMap { PremiumStoreProduct.plan(for: $0) }
|
|
|
+ .filter { $0 != .lifetime }
|
|
|
+ .max(by: { $0.rawValue < $1.rawValue })
|
|
|
+ }
|
|
|
|
|
|
private var transactionUpdatesTask: Task<Void, Never>?
|
|
|
|
|
|
@@ -266,6 +274,7 @@ final class ViewController: NSViewController {
|
|
|
private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
|
|
|
private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
|
|
|
private var paywallContinueEnabled = true
|
|
|
+ private var paywallUpgradeFlowEnabled = false
|
|
|
private var hasCompletedInitialStoreKitSync = false
|
|
|
private var hasPresentedLaunchPaywall = false
|
|
|
private var hasViewAppearedOnce = false
|
|
|
@@ -316,10 +325,12 @@ final class ViewController: NSViewController {
|
|
|
let popover = NSPopover()
|
|
|
popover.behavior = .transient
|
|
|
popover.animates = true
|
|
|
+ let showUpgradeInSettings = storeKitCoordinator.hasPremiumAccess && !storeKitCoordinator.hasLifetimeAccess
|
|
|
popover.contentViewController = SettingsMenuViewController(
|
|
|
palette: palette,
|
|
|
typography: typography,
|
|
|
darkModeEnabled: darkModeEnabled,
|
|
|
+ showUpgradeInSettings: showUpgradeInSettings,
|
|
|
onToggleDarkMode: { [weak self] enabled in
|
|
|
self?.setDarkMode(enabled)
|
|
|
},
|
|
|
@@ -737,6 +748,10 @@ private extension ViewController {
|
|
|
NSPasteboard.general.clearContents()
|
|
|
NSPasteboard.general.setString(urlString, forType: .string)
|
|
|
showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
|
|
|
+ case .upgrade:
|
|
|
+ settingsPopover?.performClose(nil)
|
|
|
+ settingsPopover = nil
|
|
|
+ showPaywall(upgradeFlow: true, preferredPlan: .lifetime)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -748,8 +763,13 @@ private extension ViewController {
|
|
|
alert.runModal()
|
|
|
}
|
|
|
|
|
|
- private func showPaywall() {
|
|
|
+ private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
|
|
|
+ paywallUpgradeFlowEnabled = upgradeFlow
|
|
|
+ if let preferredPlan {
|
|
|
+ selectedPremiumPlan = preferredPlan
|
|
|
+ }
|
|
|
if let existing = paywallWindow {
|
|
|
+ refreshPaywallStoreUI()
|
|
|
animatePaywallPresentation(existing)
|
|
|
existing.makeKeyAndOrderFront(nil)
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
@@ -866,6 +886,16 @@ private extension ViewController {
|
|
|
|
|
|
private func paywallOfferText(for plan: PremiumPlan) -> String {
|
|
|
if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ if storeKitCoordinator.hasLifetimeAccess {
|
|
|
+ return "Lifetime premium is active on this Apple ID."
|
|
|
+ }
|
|
|
+ if paywallUpgradeFlowEnabled {
|
|
|
+ let currentPlanName = storeKitCoordinator.activeNonLifetimePlan?.displayName ?? "Premium"
|
|
|
+ if plan == .lifetime {
|
|
|
+ return "Current plan: \(currentPlanName). Tap Continue to upgrade to Lifetime."
|
|
|
+ }
|
|
|
+ return "Current plan: \(currentPlanName). Select Lifetime to upgrade."
|
|
|
+ }
|
|
|
return "Premium is active on this Apple ID."
|
|
|
}
|
|
|
let productID = PremiumStoreProduct.productID(for: plan)
|
|
|
@@ -1052,10 +1082,20 @@ private extension ViewController {
|
|
|
paywallContinueButton?.alphaValue = 0.75
|
|
|
return
|
|
|
}
|
|
|
- if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ if storeKitCoordinator.hasLifetimeAccess {
|
|
|
paywallContinueEnabled = false
|
|
|
paywallContinueLabel?.stringValue = "Premium Active"
|
|
|
paywallContinueButton?.alphaValue = 0.75
|
|
|
+ } else if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess {
|
|
|
+ if selectedPremiumPlan == .lifetime {
|
|
|
+ paywallContinueEnabled = true
|
|
|
+ paywallContinueLabel?.stringValue = "Continue"
|
|
|
+ paywallContinueButton?.alphaValue = 1.0
|
|
|
+ } else {
|
|
|
+ paywallContinueEnabled = false
|
|
|
+ paywallContinueLabel?.stringValue = "Select Lifetime to Upgrade"
|
|
|
+ paywallContinueButton?.alphaValue = 0.75
|
|
|
+ }
|
|
|
} else {
|
|
|
paywallContinueEnabled = true
|
|
|
paywallContinueLabel?.stringValue = "Continue"
|
|
|
@@ -2736,6 +2776,17 @@ private extension ViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private extension PremiumPlan {
|
|
|
+ var displayName: String {
|
|
|
+ switch self {
|
|
|
+ case .weekly: return "Weekly"
|
|
|
+ case .monthly: return "Monthly"
|
|
|
+ case .yearly: return "Yearly"
|
|
|
+ case .lifetime: return "Lifetime"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
extension ViewController: NSTextFieldDelegate {
|
|
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
|
if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
|
|
@@ -2751,6 +2802,7 @@ extension ViewController: NSWindowDelegate {
|
|
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
|
|
if closingWindow === paywallWindow {
|
|
|
paywallWindow = nil
|
|
|
+ paywallUpgradeFlowEnabled = false
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -3258,6 +3310,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
palette: Palette,
|
|
|
typography: Typography,
|
|
|
darkModeEnabled: Bool,
|
|
|
+ showUpgradeInSettings: Bool,
|
|
|
onToggleDarkMode: @escaping (Bool) -> Void,
|
|
|
onAction: @escaping (SettingsAction) -> Void
|
|
|
) {
|
|
|
@@ -3266,7 +3319,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
self.onToggleDarkMode = onToggleDarkMode
|
|
|
self.onAction = onAction
|
|
|
super.init(nibName: nil, bundle: nil)
|
|
|
- self.view = makeView(darkModeEnabled: darkModeEnabled)
|
|
|
+ self.view = makeView(darkModeEnabled: darkModeEnabled, showUpgradeInSettings: showUpgradeInSettings)
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
@@ -3278,7 +3331,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
darkToggle?.state = enabled ? .on : .off
|
|
|
}
|
|
|
|
|
|
- private func makeView(darkModeEnabled: Bool) -> NSView {
|
|
|
+ private func makeView(darkModeEnabled: Bool, showUpgradeInSettings: Bool) -> NSView {
|
|
|
let root = NSView()
|
|
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
@@ -3312,6 +3365,9 @@ private final class SettingsMenuViewController: NSViewController {
|
|
|
stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
|
|
|
stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
|
|
|
stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
|
|
|
+ if showUpgradeInSettings {
|
|
|
+ stack.addArrangedSubview(settingsActionRow(icon: "⬆︎", title: "Upgrade", action: .upgrade))
|
|
|
+ }
|
|
|
|
|
|
for v in stack.arrangedSubviews {
|
|
|
v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|