|
@@ -30,6 +30,7 @@ private enum SettingsAction: Int {
|
|
30
|
case support = 2
|
30
|
case support = 2
|
|
31
|
case moreApps = 3
|
31
|
case moreApps = 3
|
|
32
|
case shareApp = 4
|
32
|
case shareApp = 4
|
|
|
|
33
|
+ case upgrade = 5
|
|
33
|
}
|
34
|
}
|
|
34
|
|
35
|
|
|
35
|
private enum PremiumPlan: Int {
|
36
|
private enum PremiumPlan: Int {
|
|
@@ -84,6 +85,13 @@ private final class StoreKitCoordinator {
|
|
84
|
var onEntitlementsChanged: ((Bool) -> Void)?
|
85
|
var onEntitlementsChanged: ((Bool) -> Void)?
|
|
85
|
|
86
|
|
|
86
|
var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
|
87
|
var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
|
|
|
|
88
|
+ var hasLifetimeAccess: Bool { activeEntitlementProductIDs.contains(PremiumStoreProduct.lifetime) }
|
|
|
|
89
|
+ var activeNonLifetimePlan: PremiumPlan? {
|
|
|
|
90
|
+ activeEntitlementProductIDs
|
|
|
|
91
|
+ .compactMap { PremiumStoreProduct.plan(for: $0) }
|
|
|
|
92
|
+ .filter { $0 != .lifetime }
|
|
|
|
93
|
+ .max(by: { $0.rawValue < $1.rawValue })
|
|
|
|
94
|
+ }
|
|
87
|
|
95
|
|
|
88
|
private var transactionUpdatesTask: Task<Void, Never>?
|
96
|
private var transactionUpdatesTask: Task<Void, Never>?
|
|
89
|
|
97
|
|
|
@@ -266,6 +274,7 @@ final class ViewController: NSViewController {
|
|
266
|
private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
|
274
|
private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
|
|
267
|
private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
|
275
|
private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
|
|
268
|
private var paywallContinueEnabled = true
|
276
|
private var paywallContinueEnabled = true
|
|
|
|
277
|
+ private var paywallUpgradeFlowEnabled = false
|
|
269
|
private var hasCompletedInitialStoreKitSync = false
|
278
|
private var hasCompletedInitialStoreKitSync = false
|
|
270
|
private var hasPresentedLaunchPaywall = false
|
279
|
private var hasPresentedLaunchPaywall = false
|
|
271
|
private var hasViewAppearedOnce = false
|
280
|
private var hasViewAppearedOnce = false
|
|
@@ -316,10 +325,12 @@ final class ViewController: NSViewController {
|
|
316
|
let popover = NSPopover()
|
325
|
let popover = NSPopover()
|
|
317
|
popover.behavior = .transient
|
326
|
popover.behavior = .transient
|
|
318
|
popover.animates = true
|
327
|
popover.animates = true
|
|
|
|
328
|
+ let showUpgradeInSettings = storeKitCoordinator.hasPremiumAccess && !storeKitCoordinator.hasLifetimeAccess
|
|
319
|
popover.contentViewController = SettingsMenuViewController(
|
329
|
popover.contentViewController = SettingsMenuViewController(
|
|
320
|
palette: palette,
|
330
|
palette: palette,
|
|
321
|
typography: typography,
|
331
|
typography: typography,
|
|
322
|
darkModeEnabled: darkModeEnabled,
|
332
|
darkModeEnabled: darkModeEnabled,
|
|
|
|
333
|
+ showUpgradeInSettings: showUpgradeInSettings,
|
|
323
|
onToggleDarkMode: { [weak self] enabled in
|
334
|
onToggleDarkMode: { [weak self] enabled in
|
|
324
|
self?.setDarkMode(enabled)
|
335
|
self?.setDarkMode(enabled)
|
|
325
|
},
|
336
|
},
|
|
@@ -737,6 +748,10 @@ private extension ViewController {
|
|
737
|
NSPasteboard.general.clearContents()
|
748
|
NSPasteboard.general.clearContents()
|
|
738
|
NSPasteboard.general.setString(urlString, forType: .string)
|
749
|
NSPasteboard.general.setString(urlString, forType: .string)
|
|
739
|
showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
|
750
|
showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
|
|
|
|
751
|
+ case .upgrade:
|
|
|
|
752
|
+ settingsPopover?.performClose(nil)
|
|
|
|
753
|
+ settingsPopover = nil
|
|
|
|
754
|
+ showPaywall(upgradeFlow: true, preferredPlan: .lifetime)
|
|
740
|
}
|
755
|
}
|
|
741
|
}
|
756
|
}
|
|
742
|
|
757
|
|
|
@@ -748,8 +763,13 @@ private extension ViewController {
|
|
748
|
alert.runModal()
|
763
|
alert.runModal()
|
|
749
|
}
|
764
|
}
|
|
750
|
|
765
|
|
|
751
|
- private func showPaywall() {
|
|
|
|
|
|
766
|
+ private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
|
|
|
|
767
|
+ paywallUpgradeFlowEnabled = upgradeFlow
|
|
|
|
768
|
+ if let preferredPlan {
|
|
|
|
769
|
+ selectedPremiumPlan = preferredPlan
|
|
|
|
770
|
+ }
|
|
752
|
if let existing = paywallWindow {
|
771
|
if let existing = paywallWindow {
|
|
|
|
772
|
+ refreshPaywallStoreUI()
|
|
753
|
animatePaywallPresentation(existing)
|
773
|
animatePaywallPresentation(existing)
|
|
754
|
existing.makeKeyAndOrderFront(nil)
|
774
|
existing.makeKeyAndOrderFront(nil)
|
|
755
|
NSApp.activate(ignoringOtherApps: true)
|
775
|
NSApp.activate(ignoringOtherApps: true)
|
|
@@ -866,6 +886,16 @@ private extension ViewController {
|
|
866
|
|
886
|
|
|
867
|
private func paywallOfferText(for plan: PremiumPlan) -> String {
|
887
|
private func paywallOfferText(for plan: PremiumPlan) -> String {
|
|
868
|
if storeKitCoordinator.hasPremiumAccess {
|
888
|
if storeKitCoordinator.hasPremiumAccess {
|
|
|
|
889
|
+ if storeKitCoordinator.hasLifetimeAccess {
|
|
|
|
890
|
+ return "Lifetime premium is active on this Apple ID."
|
|
|
|
891
|
+ }
|
|
|
|
892
|
+ if paywallUpgradeFlowEnabled {
|
|
|
|
893
|
+ let currentPlanName = storeKitCoordinator.activeNonLifetimePlan?.displayName ?? "Premium"
|
|
|
|
894
|
+ if plan == .lifetime {
|
|
|
|
895
|
+ return "Current plan: \(currentPlanName). Tap Continue to upgrade to Lifetime."
|
|
|
|
896
|
+ }
|
|
|
|
897
|
+ return "Current plan: \(currentPlanName). Select Lifetime to upgrade."
|
|
|
|
898
|
+ }
|
|
869
|
return "Premium is active on this Apple ID."
|
899
|
return "Premium is active on this Apple ID."
|
|
870
|
}
|
900
|
}
|
|
871
|
let productID = PremiumStoreProduct.productID(for: plan)
|
901
|
let productID = PremiumStoreProduct.productID(for: plan)
|
|
@@ -1052,10 +1082,20 @@ private extension ViewController {
|
|
1052
|
paywallContinueButton?.alphaValue = 0.75
|
1082
|
paywallContinueButton?.alphaValue = 0.75
|
|
1053
|
return
|
1083
|
return
|
|
1054
|
}
|
1084
|
}
|
|
1055
|
- if storeKitCoordinator.hasPremiumAccess {
|
|
|
|
|
|
1085
|
+ if storeKitCoordinator.hasLifetimeAccess {
|
|
1056
|
paywallContinueEnabled = false
|
1086
|
paywallContinueEnabled = false
|
|
1057
|
paywallContinueLabel?.stringValue = "Premium Active"
|
1087
|
paywallContinueLabel?.stringValue = "Premium Active"
|
|
1058
|
paywallContinueButton?.alphaValue = 0.75
|
1088
|
paywallContinueButton?.alphaValue = 0.75
|
|
|
|
1089
|
+ } else if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess {
|
|
|
|
1090
|
+ if selectedPremiumPlan == .lifetime {
|
|
|
|
1091
|
+ paywallContinueEnabled = true
|
|
|
|
1092
|
+ paywallContinueLabel?.stringValue = "Continue"
|
|
|
|
1093
|
+ paywallContinueButton?.alphaValue = 1.0
|
|
|
|
1094
|
+ } else {
|
|
|
|
1095
|
+ paywallContinueEnabled = false
|
|
|
|
1096
|
+ paywallContinueLabel?.stringValue = "Select Lifetime to Upgrade"
|
|
|
|
1097
|
+ paywallContinueButton?.alphaValue = 0.75
|
|
|
|
1098
|
+ }
|
|
1059
|
} else {
|
1099
|
} else {
|
|
1060
|
paywallContinueEnabled = true
|
1100
|
paywallContinueEnabled = true
|
|
1061
|
paywallContinueLabel?.stringValue = "Continue"
|
1101
|
paywallContinueLabel?.stringValue = "Continue"
|
|
@@ -2736,6 +2776,17 @@ private extension ViewController {
|
|
2736
|
}
|
2776
|
}
|
|
2737
|
}
|
2777
|
}
|
|
2738
|
|
2778
|
|
|
|
|
2779
|
+private extension PremiumPlan {
|
|
|
|
2780
|
+ var displayName: String {
|
|
|
|
2781
|
+ switch self {
|
|
|
|
2782
|
+ case .weekly: return "Weekly"
|
|
|
|
2783
|
+ case .monthly: return "Monthly"
|
|
|
|
2784
|
+ case .yearly: return "Yearly"
|
|
|
|
2785
|
+ case .lifetime: return "Lifetime"
|
|
|
|
2786
|
+ }
|
|
|
|
2787
|
+ }
|
|
|
|
2788
|
+}
|
|
|
|
2789
|
+
|
|
2739
|
extension ViewController: NSTextFieldDelegate {
|
2790
|
extension ViewController: NSTextFieldDelegate {
|
|
2740
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
2791
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
|
2741
|
if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
2792
|
if control === browseAddressField, commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
|
@@ -2751,6 +2802,7 @@ extension ViewController: NSWindowDelegate {
|
|
2751
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
2802
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
|
2752
|
if closingWindow === paywallWindow {
|
2803
|
if closingWindow === paywallWindow {
|
|
2753
|
paywallWindow = nil
|
2804
|
paywallWindow = nil
|
|
|
|
2805
|
+ paywallUpgradeFlowEnabled = false
|
|
2754
|
}
|
2806
|
}
|
|
2755
|
}
|
2807
|
}
|
|
2756
|
}
|
2808
|
}
|
|
@@ -3258,6 +3310,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
3258
|
palette: Palette,
|
3310
|
palette: Palette,
|
|
3259
|
typography: Typography,
|
3311
|
typography: Typography,
|
|
3260
|
darkModeEnabled: Bool,
|
3312
|
darkModeEnabled: Bool,
|
|
|
|
3313
|
+ showUpgradeInSettings: Bool,
|
|
3261
|
onToggleDarkMode: @escaping (Bool) -> Void,
|
3314
|
onToggleDarkMode: @escaping (Bool) -> Void,
|
|
3262
|
onAction: @escaping (SettingsAction) -> Void
|
3315
|
onAction: @escaping (SettingsAction) -> Void
|
|
3263
|
) {
|
3316
|
) {
|
|
@@ -3266,7 +3319,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
3266
|
self.onToggleDarkMode = onToggleDarkMode
|
3319
|
self.onToggleDarkMode = onToggleDarkMode
|
|
3267
|
self.onAction = onAction
|
3320
|
self.onAction = onAction
|
|
3268
|
super.init(nibName: nil, bundle: nil)
|
3321
|
super.init(nibName: nil, bundle: nil)
|
|
3269
|
- self.view = makeView(darkModeEnabled: darkModeEnabled)
|
|
|
|
|
|
3322
|
+ self.view = makeView(darkModeEnabled: darkModeEnabled, showUpgradeInSettings: showUpgradeInSettings)
|
|
3270
|
}
|
3323
|
}
|
|
3271
|
|
3324
|
|
|
3272
|
@available(*, unavailable)
|
3325
|
@available(*, unavailable)
|
|
@@ -3278,7 +3331,7 @@ private final class SettingsMenuViewController: NSViewController {
|
|
3278
|
darkToggle?.state = enabled ? .on : .off
|
3331
|
darkToggle?.state = enabled ? .on : .off
|
|
3279
|
}
|
3332
|
}
|
|
3280
|
|
3333
|
|
|
3281
|
- private func makeView(darkModeEnabled: Bool) -> NSView {
|
|
|
|
|
|
3334
|
+ private func makeView(darkModeEnabled: Bool, showUpgradeInSettings: Bool) -> NSView {
|
|
3282
|
let root = NSView()
|
3335
|
let root = NSView()
|
|
3283
|
root.translatesAutoresizingMaskIntoConstraints = false
|
3336
|
root.translatesAutoresizingMaskIntoConstraints = false
|
|
3284
|
|
3337
|
|
|
@@ -3312,6 +3365,9 @@ private final class SettingsMenuViewController: NSViewController {
|
|
3312
|
stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
|
3365
|
stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
|
|
3313
|
stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
|
3366
|
stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
|
|
3314
|
stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
|
3367
|
stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
|
|
|
|
3368
|
+ if showUpgradeInSettings {
|
|
|
|
3369
|
+ stack.addArrangedSubview(settingsActionRow(icon: "⬆︎", title: "Upgrade", action: .upgrade))
|
|
|
|
3370
|
+ }
|
|
3315
|
|
3371
|
|
|
3316
|
for v in stack.arrangedSubviews {
|
3372
|
for v in stack.arrangedSubviews {
|
|
3317
|
v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
3373
|
v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|